You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/04/25 05:16:51 UTC
(airavata-portals) 34/34: fix(launcher): deferred review polish (deepcopy, type hints, test gaps, aria-labels)
This is an automated email from the ASF dual-hosted git repository.
yasithdev pushed a commit to branch feat/generic-experiment-launcher
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit b14365110c80e53eb9e3f0cebfc88d8fe7f7df8f
Author: yasithdev <ya...@icloud.com>
AuthorDate: Sat Apr 25 01:15:39 2026 -0400
fix(launcher): deferred review polish (deepcopy, type hints, test gaps, aria-labels)
---
.../django_airavata/apps/api/launcher_client.py | 19 +++++-----
.../apps/api/tests/test_launcher_client.py | 40 ++++++++++++++++++++++
.../js/components/launch/ExperimentMetaHeader.vue | 3 ++
.../tests/unit/stores/launch.spec.ts | 28 +++++++++++++++
4 files changed, 81 insertions(+), 9 deletions(-)
diff --git a/airavata-django-portal/django_airavata/apps/api/launcher_client.py b/airavata-django-portal/django_airavata/apps/api/launcher_client.py
index b25ca5e60..514776474 100644
--- a/airavata-django-portal/django_airavata/apps/api/launcher_client.py
+++ b/airavata-django-portal/django_airavata/apps/api/launcher_client.py
@@ -9,6 +9,7 @@ stub and the real client. Tests should mock at the ``get_client`` boundary.
from __future__ import annotations
+import copy
from typing import Any, Protocol
from django.conf import settings
@@ -55,7 +56,7 @@ class _StubClient:
self.user_token = user_token
def list_applications(self, *, category: str | None, search: str | None) -> list[dict[str, Any]]:
- results = list(_STUB_APPS)
+ results = [copy.deepcopy(a) for a in _STUB_APPS]
if category:
results = [a for a in results if a["category"] == category]
if search:
@@ -66,7 +67,7 @@ class _StubClient:
def get_application(self, *, app_id: str) -> dict[str, Any]:
for a in _STUB_APPS:
if a["app_id"] == app_id:
- return a
+ return copy.deepcopy(a)
raise LookupError(f"unknown app_id {app_id!r}")
def get_project_resource_profile(self, *, project_id: str) -> dict[str, Any]:
@@ -152,25 +153,25 @@ class _RealClient:
def __init__(self, user_token: str) -> None:
self.user_token = user_token
- def list_applications(self, *, category, search):
+ def list_applications(self, *, category: str | None, search: str | None) -> list[dict[str, Any]]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def get_application(self, *, app_id):
+ def get_application(self, *, app_id: str) -> dict[str, Any]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def get_project_resource_profile(self, *, project_id):
+ def get_project_resource_profile(self, *, project_id: str) -> dict[str, Any]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def list_user_storages(self):
+ def list_user_storages(self) -> list[dict[str, Any]]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def list_projects(self):
+ def list_projects(self) -> list[dict[str, Any]]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def generate_preview(self, draft):
+ def generate_preview(self, draft: dict[str, Any]) -> dict[str, Any]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
- def launch_experiment(self, draft):
+ def launch_experiment(self, draft: dict[str, Any]) -> dict[str, Any]:
raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)")
diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py
index 2ba428000..77426d2a4 100644
--- a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py
+++ b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py
@@ -57,3 +57,43 @@ class StubLauncherClientTest(TestCase):
self.assertTrue(len(projects) >= 1)
self.assertIn("project_id", projects[0])
self.assertIn("name", projects[0])
+
+ @override_settings(LAUNCHER_CLIENT_STUB=True)
+ def test_generate_preview_emits_slurm_script_for_bridges(self):
+ client = launcher_client.get_client(user_token="ignored")
+ draft = {
+ "name": "x",
+ "project_id": "proj-stub",
+ "app_id": "app-stub",
+ "interface_name": "run",
+ "inputs": {},
+ "outputs": {},
+ "runtime": {
+ "compute_resource_id": "bridges-2",
+ "partition": "RM",
+ "walltime": "01:00:00",
+ "nodes": 1,
+ "cpus_per_node": 8,
+ },
+ }
+ preview = client.generate_preview(draft)
+ self.assertTrue(preview["invocation_command"].startswith("sbatch"))
+ self.assertIn("#SBATCH", preview["script_contents"])
+
+ @override_settings(LAUNCHER_CLIENT_STUB=True)
+ def test_list_applications_filters_by_category(self):
+ client = launcher_client.get_client(user_token="ignored")
+ results = client.list_applications(category="Molecular Dynamics", search=None)
+ self.assertTrue(len(results) >= 1)
+ self.assertTrue(all(a["category"] == "Molecular Dynamics" for a in results))
+
+ @override_settings(LAUNCHER_CLIENT_STUB=True)
+ def test_list_applications_filters_by_search(self):
+ client = launcher_client.get_client(user_token="ignored")
+ results = client.list_applications(category=None, search="namd")
+ self.assertTrue(any("NAMD" in a["name"] for a in results))
+
+ @override_settings(LAUNCHER_CLIENT_STUB=True)
+ def test_list_applications_search_returns_empty_when_no_match(self):
+ client = launcher_client.get_client(user_token="ignored")
+ self.assertEqual(client.list_applications(category=None, search="nonexistent-app"), [])
diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
index 2e9e030e2..b2300dbd7 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
+++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue
@@ -3,6 +3,7 @@
<div class="col-md-6">
<input
data-test="exp-name"
+ aria-label="Experiment name"
class="form-control"
:value="store.draft.name"
placeholder="Experiment name"
@@ -13,6 +14,7 @@
<div class="col-md-6">
<select
data-test="exp-project"
+ aria-label="Project"
class="form-select"
:value="store.draft.project_id ?? ''"
@change="onProject(($event.target as HTMLSelectElement).value)"
@@ -26,6 +28,7 @@
<div class="col-12">
<textarea
data-test="exp-description"
+ aria-label="Description"
class="form-control"
rows="2"
:value="store.draft.description"
diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
index ad073e075..3b3527233 100644
--- a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
+++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts
@@ -63,6 +63,34 @@ describe("useLaunchStore", () => {
s.setMeta({ name: "x", project_id: "p1", description: "" });
expect(s.draftHash).toBe(h2);
});
+
+ it("treats non-required null inputs as valid for tab1", () => {
+ const s = useLaunchStore();
+ const iface = {
+ name: "run",
+ inputs: [
+ { name: "a", type: "int" as const, required: true },
+ { name: "b", type: "int" as const, required: false },
+ ],
+ outputs: [],
+ };
+ s.setMeta({ name: "x", project_id: "p1", description: "" });
+ s.pickApp({ app_id: "x", name: "X", category: "C",
+ content: { kind: "github", url: "g" }, interfaces: [iface] });
+ s.pickInterface("run");
+ s.setInput("a", 1);
+ // 'b' deliberately unset (null), but optional
+ expect(s.tab1Valid).toBe(true);
+ });
+
+ it("rejects empty walltime in tab2Valid", () => {
+ const s = useLaunchStore();
+ s.setRuntime({
+ compute_resource_id: "cr-1", partition: "RM",
+ walltime: "", nodes: 1, cpus_per_node: 8,
+ });
+ expect(s.tab2Valid).toBe(false);
+ });
});
describe("setters and reset", () => {