You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2021/07/30 18:35:36 UTC

[airavata-django-portal] branch develop updated (9bb685a -> 307e518)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from 9bb685a  AIRAVATA-3492 Handle deselecting the default queue in ApplicationDeploymentEditor
     add c722afa  Updated output view provider portion of tutorial to use cookiecutter
     add 61de74e  Updated custom django app portion of tutorial to use cookiecutter
     add 5219107  updating notes on viewing final solution
     add 5f0ee98  tutorial instructions on installing Python and creating venv on all OSes
     add 8a22eec  tutorial: minor fixes
     add f63d5c8  tutorial: rename to "custom ui tutorial"
     add 3b11429  tutorial: updated screenshots
     add 31b34c3  Compatibility for building JS on Windows
     add 0236c1f  tutorial: Python on Windows instructions
     add 6921aaf  Merge pull request #62 from apache/python39
     add 722fa98  Merge pull request #63 from apache/airavata-3465
     add 564a8b1  Merge branch 'master' of https://github.com/apache/airavata-django-portal
     add 2169ef7  tutorial: tabbed instructions for different OS/environments
     add 31a8913  tutorial: update tab style to match admonitions
     add 2d2b016  tutorial: adding local python instructions for mac/linux
     add 33bbcfb  tutorial: cleaned up windows home guidance
     add 6f9048f  tutorial: windows instructions equivalent to `cd $HOME`
     add 942d997  tutorial: tweak to windows instructions
     add c024dbc  tutorial: updating host for input customization section
     add de23f6e  tutorial: rename solution repo to prevent collisions
     add 1c6b7d7  tutorial: fixing broken links
     add 316d06c  tutorial: adding a step to wait until docker container starts up
     add fe61b8c  tutorial: updating name of GRP, compute resource
     add 461b9a5  tutorial: adding some clarification about which inputs are configured
     add fe4ef5f  tutorial: updating order of output view provider steps
     add 6707998  AIRAVATA-3420 Switch to published versions of SDK
     add b2317c9  Adds instructions on building multi-arch Docker images
     add 3e15aec  tutorial: expanded guidance and verification steps for installing prerequisites
     add 305b02c  Updated docs with instructions on how to use cookiecutter project templates
     add e5f76f6  tutorial: updating instructions for Windows PowerShell
     add e6f8735  tutorial: clarify why we generate custom Django app code as prerequisite to custom output view provider
     new ec660ed  Merge branch 'master' into develop
     new 0dabd4b  AIRAVATA-3491 Utility method for constructing simple Experiment object
     new fcf01b9  AIRAVATA-3491 Unit tests for ExperimentUtils.js
     new 9eed16f  AIRAVATA-3491 Accept application module id as input too
     new 52244a9  AIRAVATA-3491 Add applicationInterfaceId option to createExperiment
     new 307e518  AIRAVATA-3491: tutorial: update to use createExperiment utility

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 README.md                                          |   16 +
 build_js.bat                                       |   36 +
 django_airavata/apps/api/package.json              |    4 +-
 .../api/static/django_airavata_api/js/index.js     |    2 +
 .../js/models/ApplicationInterfaceDefinition.js    |   11 +
 .../js/utils/ExperimentUtils.js                    |  177 +++
 .../tests/utils/ExperimentUtils.test.js            |  589 ++++++++++
 django_airavata/static/common/package.json         |    3 +-
 django_airavata/static/common/yarn.lock            |  387 ++++---
 docs/assets/css/pymdownx.tabbed.css                |   47 +
 docs/dev/custom_django_app.md                      |  295 ++---
 docs/dev/custom_output_view_provider.md            |  121 +-
 ...{gateways_tutorial.md => custom_ui_tutorial.md} | 1180 ++++++++++++--------
 .../screenshots/custom-ui/custom-app-menu.png      |  Bin 0 -> 177809 bytes
 .../gaussian-output-view-providers-json.png        |  Bin 0 -> 31419 bytes
 .../screenshots/custom-ui/settings_menu.png        |  Bin 0 -> 181704 bytes
 mkdocs.yml                                         |    6 +-
 requirements-dev.txt                               |    3 +-
 setup.py                                           |    1 -
 19 files changed, 2073 insertions(+), 805 deletions(-)
 create mode 100644 build_js.bat
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
 create mode 100644 docs/assets/css/pymdownx.tabbed.css
 rename docs/tutorial/{gateways_tutorial.md => custom_ui_tutorial.md} (52%)
 create mode 100644 docs/tutorial/screenshots/custom-ui/custom-app-menu.png
 create mode 100644 docs/tutorial/screenshots/custom-ui/gaussian-output-view-providers-json.png
 create mode 100644 docs/tutorial/screenshots/custom-ui/settings_menu.png

[airavata-django-portal] 01/06: Merge branch 'master' into develop

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit ec660ed48e1bed2c776d37c89d705e4a1548e57e
Merge: 9bb685a e6f8735
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jul 30 13:25:57 2021 -0400

    Merge branch 'master' into develop

 README.md                                          |   16 +
 build_js.bat                                       |   36 +
 django_airavata/static/common/package.json         |    3 +-
 django_airavata/static/common/yarn.lock            |  387 +++++---
 docs/assets/css/pymdownx.tabbed.css                |   47 +
 docs/dev/custom_django_app.md                      |  295 +++---
 docs/dev/custom_output_view_provider.md            |  121 ++-
 ...{gateways_tutorial.md => custom_ui_tutorial.md} | 1023 ++++++++++++--------
 .../screenshots/custom-ui/custom-app-menu.png      |  Bin 0 -> 177809 bytes
 .../gaussian-output-view-providers-json.png        |  Bin 0 -> 31419 bytes
 .../screenshots/custom-ui/settings_menu.png        |  Bin 0 -> 181704 bytes
 mkdocs.yml                                         |    6 +-
 requirements-dev.txt                               |    3 +-
 setup.py                                           |    1 -
 14 files changed, 1221 insertions(+), 717 deletions(-)


[airavata-django-portal] 05/06: AIRAVATA-3491 Add applicationInterfaceId option to createExperiment

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 52244a9f9fab9541bdb6ca90a0b253652983a2d8
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jul 30 14:30:25 2021 -0400

    AIRAVATA-3491 Add applicationInterfaceId option to createExperiment
---
 .../js/utils/ExperimentUtils.js                    | 15 ++++++++--
 .../tests/utils/ExperimentUtils.test.js            | 32 +++++++++++++++++++++-
 2 files changed, 44 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
index 0088e27..be91f0a 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -3,12 +3,17 @@ import { services } from "../index";
 const createExperiment = async function ({
   applicationName, // name of the application interface (usually the same as the application module)
   applicationId, // the id of the application module
+  applicationInterfaceId, // the id of the application interface
   computeResourceName,
   experimentName,
   experimentInputs,
 } = {}) {
   let applicationInterface = null;
-  if (applicationId) {
+  if (applicationInterfaceId) {
+    applicationInterface = await loadApplicationInterfaceById(
+      applicationInterfaceId
+    );
+  } else if (applicationId) {
     applicationInterface = await loadApplicationInterfaceByApplicationModuleId(
       applicationId
     );
@@ -17,7 +22,7 @@ const createExperiment = async function ({
       applicationName
     );
   } else {
-    throw new Error("Either applicationName or applicationId is required");
+    throw new Error("Either applicationInterfaceId or applicationId or applicationName is required");
   }
   const applicationModuleId = applicationInterface.applicationModuleId;
   let computeResourceId = null;
@@ -90,6 +95,12 @@ const loadApplicationInterfaceByName = async function (applicationName) {
   return applicationInterface;
 };
 
+const loadApplicationInterfaceById = async function (applicationInterfaceId) {
+  return await services.ApplicationInterfaceService.retrieve({
+    lookup: applicationInterfaceId,
+  });
+};
+
 const loadApplicationInterfaceByApplicationModuleId = async function (
   applicationId
 ) {
diff --git a/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
index 8e64229..f57e4af 100644
--- a/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
+++ b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
@@ -8,6 +8,10 @@ import { createExperiment } from "../../js/utils/ExperimentUtils";
 // Mock out 'index' so that RESTful service calls can be mocked
 jest.mock("../../js/index");
 
+beforeEach(() => {
+  jest.resetAllMocks();
+});
+
 test("error thrown when no applicationName given", async () => {
   try {
     expect.assertions(2);
@@ -15,7 +19,7 @@ test("error thrown when no applicationName given", async () => {
   } catch (e) {
     expect(e).toBeInstanceOf(Error);
     expect(e.message).toEqual(
-      "Either applicationName or applicationId is required"
+      "Either applicationInterfaceId or applicationId or applicationName is required"
     );
   }
 });
@@ -65,6 +69,32 @@ test("verify if applicationId and applicationName are given, applicationInterfac
   }
 });
 
+test("verify if applicationInterfaceId and applicationId and applicationName are given, applicationInterface is loaded with applicationId", async () => {
+  services.ApplicationInterfaceService.retrieve.mockResolvedValue(
+    new ApplicationInterfaceDefinition({
+      applicationInterfaceId: "Foo_interface1",
+      applicationName: "Foo",
+      applicationModules: ["Foo_module1"],
+    })
+  );
+  try {
+    expect.assertions(3);
+    await createExperiment({
+      applicationInterfaceId: "Foo_interface1",
+      applicationId: "Foo_module1",
+      applicationName: "Foo",
+    });
+  } catch (e) {
+    expect(services.ApplicationModuleService.getApplicationInterface).not.toHaveBeenCalled();
+    expect(services.ApplicationInterfaceService.list).not.toHaveBeenCalled();
+    expect(
+      services.ApplicationInterfaceService.retrieve
+    ).toHaveBeenCalledWith({
+      lookup: "Foo_interface1",
+    });
+  }
+});
+
 test("error thrown when no computeResourceName given", async () => {
   services.ApplicationInterfaceService.list.mockResolvedValue([
     new ApplicationInterfaceDefinition({

[airavata-django-portal] 02/06: AIRAVATA-3491 Utility method for constructing simple Experiment object

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 0dabd4b08faa58a9ddc0687f36b83f2125617709
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Jul 28 11:54:54 2021 -0400

    AIRAVATA-3491 Utility method for constructing simple Experiment object
---
 django_airavata/apps/api/package.json              |   4 +-
 .../api/static/django_airavata_api/js/index.js     |   2 +
 .../js/models/ApplicationInterfaceDefinition.js    |  10 ++
 .../js/utils/ExperimentUtils.js                    | 151 +++++++++++++++++++++
 4 files changed, 165 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/api/package.json b/django_airavata/apps/api/package.json
index 5cc5385..8533c3d 100644
--- a/django_airavata/apps/api/package.json
+++ b/django_airavata/apps/api/package.json
@@ -43,8 +43,8 @@
     "root": true,
     "extends": "eslint:recommended",
     "parserOptions": {
-      "ecmaVersion": 6,
-      "sourceType": "module"
+      "sourceType": "module",
+      "ecmaVersion": 2017
     },
     "env": {
       "browser": true,
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index 59421a4..7728bea 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -51,6 +51,7 @@ import ServiceFactory from "./services/ServiceFactory";
 
 import Session from "./session/Session";
 
+import ExperimentUtils from "./utils/ExperimentUtils";
 import FetchUtils from "./utils/FetchUtils";
 import PaginationIterator from "./utils/PaginationIterator";
 import StringUtils from "./utils/StringUtils";
@@ -155,6 +156,7 @@ const session = {
 };
 
 const utils = {
+  ExperimentUtils,
   FetchUtils,
   PaginationIterator,
   StringUtils,
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
index 5fe19cb..ee919d4 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
@@ -78,4 +78,14 @@ export default class ApplicationInterfaceDefinition extends BaseModel {
     experiment.executionId = this.applicationInterfaceId;
     return experiment;
   }
+
+  get applicationModuleId() {
+    if (!this.applicationModules || this.applicationModules.length > 1) {
+      throw new Error(
+        `No unique application module exists for interface
+        ${this.applicationName}: modules=${this.applicationModules}`
+      );
+    }
+    return this.applicationModules[0];
+  }
 }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
new file mode 100644
index 0000000..613e868
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -0,0 +1,151 @@
+import { services } from "../index";
+
+const createExperiment = async function ({
+  applicationName, // name of the application interface (usually the same as the application module)
+  computeResourceName,
+  experimentName,
+  experimentInputs,
+} = {}) {
+  let applicationInterface = null;
+  if (applicationName) {
+    applicationInterface = await loadApplicationInterfaceByName(
+      applicationName
+    );
+  } else {
+    throw new Error("applicationName is required");
+  }
+  const applicationModuleId = applicationInterface.applicationModuleId;
+  let computeResourceId = null;
+  if (computeResourceName) {
+    computeResourceId = await loadComputeResourceIdByName(computeResourceName);
+  } else {
+    throw new Error("computeResourceName is required");
+  }
+  let groupResourceProfile = await loadGroupResourceProfile(computeResourceId);
+  let deployments = await loadApplicationDeployments(
+    applicationModuleId,
+    groupResourceProfile
+  );
+  const deployment = deployments.find(
+    (d) => d.computeHostId === computeResourceId
+  );
+  if (!deployment) {
+    throw new Error(
+      `Couldn't find a deployment for compute resource ${computeResourceId}`
+    );
+  }
+  let queueDescription = await loadQueue(deployment);
+  let workspacePreferences = await loadWorkspacePreferences();
+  const projectId = workspacePreferences.most_recent_project_id;
+
+  const experiment = applicationInterface.createExperiment();
+  if (experimentName) {
+    experiment.experimentName = experimentName;
+  } else {
+    experiment.experimentName = `${
+      applicationInterface.applicationName
+    } on ${new Date().toLocaleString([], {
+      dateStyle: "medium",
+      timeStyle: "short",
+    })}`;
+  }
+  experiment.projectId = projectId;
+  experiment.userConfigurationData.groupResourceProfileId =
+    groupResourceProfile.groupResourceProfileId;
+  experiment.userConfigurationData.computationalResourceScheduling.resourceHostId = computeResourceId;
+  experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount =
+    queueDescription.defaultCPUCount;
+  experiment.userConfigurationData.computationalResourceScheduling.nodeCount =
+    queueDescription.defaultNodeCount;
+  experiment.userConfigurationData.computationalResourceScheduling.wallTimeLimit =
+    queueDescription.defaultWalltime;
+  experiment.userConfigurationData.computationalResourceScheduling.queueName =
+    queueDescription.queueName;
+
+  if (experimentInputs) {
+    for (let input of experiment.experimentInputs) {
+      if (input.name in experimentInputs) {
+        input.value = experimentInputs[input.name];
+      }
+    }
+  }
+  return experiment;
+};
+
+const loadApplicationInterfaceByName = async function (applicationName) {
+  const applicationInterfaces = await services.ApplicationInterfaceService.list();
+  const applicationInterface = applicationInterfaces.find(
+    (ai) => ai.applicationName === applicationName
+  );
+  if (!applicationInterface) {
+    throw new Error(
+      `Could not find application with interface named ${applicationName}`
+    );
+  }
+  return applicationInterface;
+};
+
+const loadComputeResourceIdByName = async function (computeResourceName) {
+  const computeResourceNames = await services.ComputeResourceService.names();
+  for (const computeResourceId in computeResourceNames) {
+    if (
+      computeResourceNames.hasOwnProperty(computeResourceId) &&
+      computeResourceNames[computeResourceId] === computeResourceName
+    ) {
+      return computeResourceId;
+    }
+  }
+  throw new Error(
+    `Could not find compute resource with name ${computeResourceName}`
+  );
+};
+
+const loadGroupResourceProfile = async function (computeResourceId) {
+  const groupResourceProfiles = await services.GroupResourceProfileService.list();
+  const groupResourceProfile = groupResourceProfiles.find((grp) => {
+    for (let computePref of grp.computePreferences) {
+      if (computePref.computeResourceId === computeResourceId) {
+        return true;
+      }
+    }
+    return false;
+  });
+  if (!groupResourceProfile) {
+    throw new Error(
+      `Couldn't find a group resource profile for compute resource ${computeResourceId}`
+    );
+  }
+  return groupResourceProfile;
+};
+
+const loadApplicationDeployments = async function (
+  applicationModuleId,
+  groupResourceProfile
+) {
+  return await services.ApplicationDeploymentService.list({
+    appModuleId: applicationModuleId,
+    groupResourceProfileId: groupResourceProfile.groupResourceProfileId,
+  });
+};
+
+const loadQueue = async function (applicationDeployment) {
+  const queues = await services.ApplicationDeploymentService.getQueues({
+    lookup: applicationDeployment.appDeploymentId,
+  });
+  const queue = queues.find((q) => q.isDefaultQueue);
+  if (!queue) {
+    throw new Error(
+      "Couldn't find a default queue for deployment " +
+        applicationDeployment.appDeploymentId
+    );
+  }
+  return queue;
+};
+
+const loadWorkspacePreferences = async function () {
+  return await services.WorkspacePreferencesService.get();
+};
+
+export default {
+  createExperiment,
+};

[airavata-django-portal] 04/06: AIRAVATA-3491 Accept application module id as input too

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 9eed16fd9dfa72530d2d4e0a52b0ff4b5087e493
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jul 30 13:19:31 2021 -0400

    AIRAVATA-3491 Accept application module id as input too
---
 .../js/utils/ExperimentUtils.js                    | 21 +++++++++++++----
 .../tests/utils/ExperimentUtils.test.js            | 27 +++++++++++++++++++++-
 2 files changed, 42 insertions(+), 6 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
index 2e6cea0..0088e27 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -2,17 +2,22 @@ import { services } from "../index";
 
 const createExperiment = async function ({
   applicationName, // name of the application interface (usually the same as the application module)
+  applicationId, // the id of the application module
   computeResourceName,
   experimentName,
   experimentInputs,
 } = {}) {
   let applicationInterface = null;
-  if (applicationName) {
+  if (applicationId) {
+    applicationInterface = await loadApplicationInterfaceByApplicationModuleId(
+      applicationId
+    );
+  } else if (applicationName) {
     applicationInterface = await loadApplicationInterfaceByName(
       applicationName
     );
   } else {
-    throw new Error("applicationName is required");
+    throw new Error("Either applicationName or applicationId is required");
   }
   const applicationModuleId = applicationInterface.applicationModuleId;
   let computeResourceId = null;
@@ -85,6 +90,14 @@ const loadApplicationInterfaceByName = async function (applicationName) {
   return applicationInterface;
 };
 
+const loadApplicationInterfaceByApplicationModuleId = async function (
+  applicationId
+) {
+  return await services.ApplicationModuleService.getApplicationInterface({
+    lookup: applicationId,
+  });
+};
+
 const loadComputeResourceIdByName = async function (computeResourceName) {
   const computeResourceNames = await services.ComputeResourceService.names();
   for (const computeResourceId in computeResourceNames) {
@@ -146,9 +159,7 @@ const loadWorkspacePreferences = async function () {
   return await services.WorkspacePreferencesService.get();
 };
 
-export {
-  createExperiment,
-};
+export { createExperiment };
 
 export default {
   createExperiment,
diff --git a/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
index 02ecde7..8e64229 100644
--- a/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
+++ b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
@@ -14,7 +14,9 @@ test("error thrown when no applicationName given", async () => {
     await createExperiment();
   } catch (e) {
     expect(e).toBeInstanceOf(Error);
-    expect(e.message).toEqual("applicationName is required");
+    expect(e.message).toEqual(
+      "Either applicationName or applicationId is required"
+    );
   }
 });
 
@@ -40,6 +42,29 @@ test("error thrown with applicationName doesn't match any interfaces", async ()
   }
 });
 
+test("verify if applicationId and applicationName are given, applicationInterface is loaded with applicationId", async () => {
+  services.ApplicationModuleService.getApplicationInterface.mockResolvedValue(
+    new ApplicationInterfaceDefinition({
+      applicationName: "Foo",
+      applicationModules: ["Foo_module1"],
+    })
+  );
+  try {
+    expect.assertions(2);
+    await createExperiment({
+      applicationId: "Foo_module1",
+      applicationName: "Foo",
+    });
+  } catch (e) {
+    expect(services.ApplicationModuleService.list).not.toHaveBeenCalled();
+    expect(
+      services.ApplicationModuleService.getApplicationInterface
+    ).toHaveBeenCalledWith({
+      lookup: "Foo_module1",
+    });
+  }
+});
+
 test("error thrown when no computeResourceName given", async () => {
   services.ApplicationInterfaceService.list.mockResolvedValue([
     new ApplicationInterfaceDefinition({

[airavata-django-portal] 06/06: AIRAVATA-3491: tutorial: update to use createExperiment utility

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 307e51833a3165a3767d4d1554ac811100c37e96
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jul 30 14:33:35 2021 -0400

    AIRAVATA-3491: tutorial: update to use createExperiment utility
---
 docs/tutorial/custom_ui_tutorial.md | 197 +++++++++++++++++-------------------
 1 file changed, 91 insertions(+), 106 deletions(-)

diff --git a/docs/tutorial/custom_ui_tutorial.md b/docs/tutorial/custom_ui_tutorial.md
index 64ef718..9299432 100644
--- a/docs/tutorial/custom_ui_tutorial.md
+++ b/docs/tutorial/custom_ui_tutorial.md
@@ -1358,133 +1358,118 @@ The user interface should now look something like:
 
 ### Submitting an Echo job
 
-Now we'll use `AiravataAPI` to submit an Echo job. Let's take a look at what
-we'll need to do make this work.
+Now we'll use `AiravataAPI` to submit an Echo job. First we'll add the code and
+then examine it line by line to see what it is doing.
 
-1. We'll need to add a click handler to the _Run_ button that gets the selected
-   greeting value:
+1. Add the following to the end of the _scripts_ block in `hello.html`:
 
 ```javascript
-$("#run-button").click((e) => {
-    const greeting = $("#greeting-select").val();
-});
-```
+// ...
+
+// STARTING HERE
+    $("#run-button").click((e) => {
+        const greeting = $("#greeting-select").val();
+        // Construct experiment object
+        utils.ExperimentUtils.createExperiment({
+            applicationInterfaceId: appInterfaceId,
+            computeResourceName: "example-vc.jetstream-cloud.org",
+            experimentName: "Echo " + greeting,
+            experimentInputs: {
+                "Input-to-Echo": greeting
+            }
+        }).then(experiment=> {
+            // Save experiment
+            return services.ExperimentService.create({ data: experiment });
+        }).then(experiment => {
+            // Launch experiment
+            return services.ExperimentService.launch({
+                lookup: experiment.experimentId,
+            });
+        })
+    });
+// ENDING HERE
 
-2. There are a couple key pieces of information that are needed to submit a
-   computational experiment. We can use the REST API to find these. The
-   application we want to use is called Echo and it has id
-   `Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8`. We can browse the API for this
-   application using:
-   <https://testdrive.airavata.org/api/applications/Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/>.
-   First, we need the _Application Interface_ for the application, which defines
-   the inputs and outputs of the application. We can get its id by following the
-   link to `applicationInterface`:
-   <https://testdrive.airavata.org/api/applications/Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/application_interface/>.
-   We'll create an _Experiment_ instance from the _Application Interface_
-   definition:
+</script>
 
-```javascript
-const loadAppInterface = services.ApplicationInterfaceService.retrieve({
-    lookup: appInterfaceId,
-});
+{% endblock scripts %}
 ```
 
-3. Second, we need to know where and how the application is deployed. We could
-   let the user then pick where they want to run this application. For this
-   exercise we're going to hard code the resource and the application deployment
-   that will be used for executing the application, but we still need the
-   application deployment information so we can get default values for the
-   application that can be used when submitting the job to that scheduler. The
-   application deployment id we get from
-   <https://testdrive.airavata.org/api/applications/Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/application_deployments/>.
+2. Going line by line we'll now take a look at this code. We added a click
+   handler to the _Run_ button that gets the selected greeting value:
 
 ```javascript
-const appDeploymentId =
-    "example-vc.jetstream-cloud.org_Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8";
-const loadQueues = services.ApplicationDeploymentService.getQueues({
-    lookup: appDeploymentId,
+$("#run-button").click((e) => {
+    const greeting = $("#greeting-select").val();
 });
 ```
 
-4. We also need to know a few other pieces of information, like the id of the
-   compute resource which
-   <https://testdrive.airavata.org/api/applications/Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/application_deployments/>
-   also provides with the value `computeHostId`. The queue name we can get from
-   following the link from the deployment to the queues:
-   <https://testdrive.airavata.org/api/application-deployments/example-vc.jetstream-cloud.org_Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/queues/>.
-   Here we see that the `queueName` is `cloud`. We also need the account to use
-   to submit the job and that is specified via a "Group Resource Profile".
-   <https://testdrive.airavata.org/api/group-resource-profiles/> lists profiles
-   you have access to and the compute resources each profile can use for job
-   submission. We'll use the tutorial reservation one. Finally, experiments are
-   organized by projects so we'll also load the user's most recently used
-   project which is stored in the user's WorkspacePreferences:
+3. Now the code constructs an experiment object using the utility function
+   `utils.ExperimentUtils.createExperiment`. In Airavata, Experiments are
+   created from Application Interface descriptions, so we'll first pass the
+   `applicationInterfaceId`. We already have the `appInterfaceId` in the code
+   because we used this to retrieve experiment results for the Echo application.
+
+    You might wonder, how would I find this ID if I wanted to look one up for an
+    application? For this you can use the REST API and programmatically look up
+    these values in your code. Also, the Airavata Django Portal has a browseable
+    REST API and we'll take a look at that now.
+
+    If you go to <https://testdrive.airavata.org/api/applications/> (make sure
+    you are logged in first) you'll see the _Application Module List_. For each
+    entry you can click on the URL for the `applicationInterface` and get the
+    REST API response for the application interface. For example, if you click
+    on the applicationInterface link for the Echo application module,
+    <https://testdrive.airavata.org/api/applications/Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8/application_interface/>,
+    you'll then get a response that shows that the _applicationInterfaceId_ is
+    `Echo_23d67491-1bef-47bd-a0f5-faf069e09773`.
+
+    The other parameters to the `createExperiment` function are the
+    `computeResourceName` of the cluster to which we want to submit as well as
+    the name of the experiment and the experiment's input values.
 
 ```javascript
-const resourceHostId =
-    "example-vc.jetstream-cloud.org_794fd026-101a-46af-8868-5d7a86f813a1";
-const queueName = "cloud";
-const groupResourceProfileId = "fc245311-a7d1-41af-b8ae-a4142989c9a1";
-const loadWorkspacePrefs = services.WorkspacePreferencesService.get();
+        // Construct experiment object
+        utils.ExperimentUtils.createExperiment({
+            applicationInterfaceId: appInterfaceId,
+            computeResourceName: "example-vc.jetstream-cloud.org",
+            experimentName: "Echo " + greeting,
+            experimentInputs: {
+                "Input-to-Echo": greeting
+            }
+        })
 ```
 
-5. Once we have all of this information we can then create an `Experiment`
-   object then _save_ and _launch_ it. Here's the complete click handler. We add
-   the following to the end of the _scripts_ block in `hello.html`:
+4. The `createExperiment` function does a few more things behind the scenes and
+   once we run it we can take a look at the REST API calls it makes. In summary
+   `createExperiment`:
+
+    - loads the Application Interface
+    - loads the compute resource ID
+    - finds a Group Resource Profile that can be used to submit the job to the
+      given compute resource
+    - finds the Application Deployment for the application on the given compute
+      resource
+    - loads the default queue settings for that Application Deployment and uses
+      them when constructing the Experiment
+    - loads the user's most recently used Project and uses it when constructing
+      the Experiment
+    - creates an Experiment instance from the Application Interface and
+      populates its fields
+
+5. Now that the experiment object is created, we can save it
+   (`ExperimentService.create`) and then launch it (`ExperimentService.launch`).
 
 ```javascript
-// ...
-
-// STARTING HERE
-$("#run-button").click((e) => {
-    const greeting = $("#greeting-select").val();
-    const loadAppInterface = services.ApplicationInterfaceService.retrieve({
-        lookup: appInterfaceId,
-    });
-    const appDeploymentId =
-        "example-vc.jetstream-cloud.org_Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8";
-    const loadQueues = services.ApplicationDeploymentService.getQueues({
-        lookup: appDeploymentId,
-    });
-    const resourceHostId =
-        "example-vc.jetstream-cloud.org_794fd026-101a-46af-8868-5d7a86f813a1";
-    const queueName = "cloud";
-    const groupResourceProfileId = "fc245311-a7d1-41af-b8ae-a4142989c9a1";
-    const loadWorkspacePrefs = services.WorkspacePreferencesService.get();
-    Promise.all([loadAppInterface, loadWorkspacePrefs, loadQueues])
-        .then(([appInterface, workspacePrefs, queues]) => {
-            const experiment = appInterface.createExperiment();
-            experiment.experimentName = "Echo " + greeting;
-            experiment.projectId = workspacePrefs.most_recent_project_id;
-            const cloudQueue = queues.find((q) => q.queueName === queueName);
-            experiment.userConfigurationData.groupResourceProfileId =
-                groupResourceProfileId;
-            experiment.userConfigurationData.computationalResourceScheduling.resourceHostId =
-                resourceHostId;
-            experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount =
-                cloudQueue.defaultCPUCount;
-            experiment.userConfigurationData.computationalResourceScheduling.nodeCount =
-                cloudQueue.defaultNodeCount;
-            experiment.userConfigurationData.computationalResourceScheduling.wallTimeLimit =
-                cloudQueue.defaultWalltime;
-            experiment.userConfigurationData.computationalResourceScheduling.queueName =
-                queueName;
-            // Copy the selected greeting to the value of the first input
-            experiment.experimentInputs[0].value = greeting;
-
+        // ...
+        }).then(experiment=> {
+            // Save experiment
             return services.ExperimentService.create({ data: experiment });
-        })
-        .then((exp) => {
+        }).then(experiment => {
+            // Launch experiment
             return services.ExperimentService.launch({
-                lookup: exp.experimentId,
+                lookup: experiment.experimentId,
             });
-        });
-});
-// ENDING HERE
-
-</script>
-
-{% endblock scripts %}
+        })
 ```
 
 Now that we can launch the experiment we can go ahead and give it a try.

[airavata-django-portal] 03/06: AIRAVATA-3491 Unit tests for ExperimentUtils.js

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit fcf01b900a24e4c02826587cb38e6bf48f67f9e0
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jul 30 12:59:30 2021 -0400

    AIRAVATA-3491 Unit tests for ExperimentUtils.js
---
 .../js/models/ApplicationInterfaceDefinition.js    |   1 +
 .../js/utils/ExperimentUtils.js                    |   6 +-
 .../tests/utils/ExperimentUtils.test.js            | 534 +++++++++++++++++++++
 3 files changed, 540 insertions(+), 1 deletion(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
index ee919d4..6ed2eed 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
@@ -26,6 +26,7 @@ const FIELDS = [
     name: "applicationOutputs",
     type: OutputDataObjectType,
     list: true,
+    default: BaseModel.defaultNewInstance(Array),
   },
   {
     name: "archiveWorkingDirectory",
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
index 613e868..2e6cea0 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -79,7 +79,7 @@ const loadApplicationInterfaceByName = async function (applicationName) {
   );
   if (!applicationInterface) {
     throw new Error(
-      `Could not find application with interface named ${applicationName}`
+      `Could not find application interface named ${applicationName}`
     );
   }
   return applicationInterface;
@@ -146,6 +146,10 @@ const loadWorkspacePreferences = async function () {
   return await services.WorkspacePreferencesService.get();
 };
 
+export {
+  createExperiment,
+};
+
 export default {
   createExperiment,
 };
diff --git a/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
new file mode 100644
index 0000000..02ecde7
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/tests/utils/ExperimentUtils.test.js
@@ -0,0 +1,534 @@
+import { services } from "../../js/index";
+import ApplicationInterfaceDefinition from "../../js/models/ApplicationInterfaceDefinition";
+import GroupResourceProfile from "../../js/models/GroupResourceProfile";
+import ApplicationDeploymentDescription from "../../js/models/ApplicationDeploymentDescription";
+import BatchQueue from "../../js/models/BatchQueue";
+import { createExperiment } from "../../js/utils/ExperimentUtils";
+
+// Mock out 'index' so that RESTful service calls can be mocked
+jest.mock("../../js/index");
+
+test("error thrown when no applicationName given", async () => {
+  try {
+    expect.assertions(2);
+    await createExperiment();
+  } catch (e) {
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual("applicationName is required");
+  }
+});
+
+test("error thrown with applicationName doesn't match any interfaces", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "Foo",
+      applicationModules: ["Foo_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  try {
+    expect.assertions(2);
+    await createExperiment({ applicationName: "test" });
+  } catch (e) {
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual(
+      "Could not find application interface named test"
+    );
+  }
+});
+
+test("error thrown when no computeResourceName given", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  try {
+    expect.assertions(2);
+    await createExperiment({ applicationName: "test" });
+  } catch (e) {
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual("computeResourceName is required");
+  }
+});
+
+test("error thrown when computeResourceName doesn't match any compute resources", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+    "compute2.resource.org_id2": "compute2.resource.org",
+  });
+  try {
+    expect.assertions(2);
+    await createExperiment({
+      applicationName: "test",
+      computeResourceName: "nonexistent.compute.resource.org",
+    });
+  } catch (e) {
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual(
+      "Could not find compute resource with name nonexistent.compute.resource.org"
+    );
+  }
+});
+
+test("error thrown when no GRP found for compute resource", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+    "compute2.resource.org_id2": "compute2.resource.org",
+  });
+  // Have mock GRP response be an empty list
+  services.GroupResourceProfileService.list.mockResolvedValue([]);
+  try {
+    expect.assertions(2);
+    await createExperiment({
+      applicationName: "test",
+      computeResourceName: "compute1.resource.org",
+    });
+  } catch (e) {
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual(
+      "Couldn't find a group resource profile for compute resource compute1.resource.org_id1"
+    );
+  }
+});
+
+test("error thrown when no deployment found for compute resource", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+    "compute2.resource.org_id2": "compute2.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  // Just return an empty list
+  services.ApplicationDeploymentService.list.mockResolvedValue([]);
+  try {
+    expect.assertions(3);
+    await createExperiment({
+      applicationName: "test",
+      computeResourceName: "compute1.resource.org",
+    });
+  } catch (e) {
+    expect(services.ApplicationDeploymentService.list).toHaveBeenCalledWith({
+      appModuleId: "test_module1",
+      groupResourceProfileId: "groupResourceProfileId1",
+    });
+    expect(e).toBeInstanceOf(Error);
+    expect(e.message).toEqual(
+      "Couldn't find a deployment for compute resource compute1.resource.org_id1"
+    );
+  }
+});
+
+test("verify that default queue values are used in computationalResourceScheduling", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+    new ApplicationInterfaceDefinition({
+      applicationName: "Bar",
+      applicationModules: ["bar_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+    "compute2.resource.org_id2": "compute2.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue1",
+      isDefaultQueue: false,
+      defaultCPUCount: 10,
+      defaultNodeCount: 11,
+      defaultWalltime: 12,
+    }),
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+  });
+  expect(
+    experiment.userConfigurationData.computationalResourceScheduling
+      .resourceHostId
+  ).toBe("compute1.resource.org_id1");
+  expect(
+    experiment.userConfigurationData.computationalResourceScheduling
+      .totalCPUCount
+  ).toBe(20);
+  expect(
+    experiment.userConfigurationData.computationalResourceScheduling.nodeCount
+  ).toBe(21);
+  expect(
+    experiment.userConfigurationData.computationalResourceScheduling
+      .wallTimeLimit
+  ).toBe(22);
+  expect(
+    experiment.userConfigurationData.computationalResourceScheduling.queueName
+  ).toBe("queue2");
+});
+
+test("verify that experiment name is the given name", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    experimentName: "My Experiment",
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+  });
+  expect(experiment.experimentName).toBe("My Experiment");
+});
+
+test("verify that if no experiment name is given, name is based on experiment name", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+  });
+  // Date string doesn't include seconds, so it should match exactly
+  const dateString = new Date().toLocaleString([], {
+    dateStyle: "medium",
+    timeStyle: "short",
+  });
+  expect(experiment.experimentName).toBe(`test on ${dateString}`);
+});
+
+test("verify that application inputs and outputs are cloned on experiment", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+      applicationInputs: [
+        {
+          name: "appInput1",
+        },
+        {
+          name: "appInput2",
+        },
+      ],
+      applicationOutputs: [
+        {
+          name: "appOutput1",
+        },
+        {
+          name: "appOutput2",
+        },
+      ],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+  });
+  expect(
+    experiment.experimentInputs.find((i) => i.name === "appInput1")
+  ).toBeDefined();
+  expect(
+    experiment.experimentInputs.find((i) => i.name === "appInput2")
+  ).toBeDefined();
+  expect(experiment.experimentInputs.length).toBe(2);
+  expect(
+    experiment.experimentOutputs.find((i) => i.name === "appOutput1")
+  ).toBeDefined();
+  expect(
+    experiment.experimentOutputs.find((i) => i.name === "appOutput2")
+  ).toBeDefined();
+  expect(experiment.experimentOutputs.length).toBe(2);
+});
+
+test("verify that projectId is copied from preferences", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+  });
+  expect(experiment.projectId).toBe("project1");
+});
+
+test("verify that given input values are copied to experiment", async () => {
+  services.ApplicationInterfaceService.list.mockResolvedValue([
+    new ApplicationInterfaceDefinition({
+      applicationName: "test",
+      applicationModules: ["test_module1"],
+      applicationInputs: [
+        {
+          name: "appInput1",
+          value: "default1",
+        },
+        {
+          name: "appInput2",
+        },
+      ],
+    }),
+  ]);
+  services.ComputeResourceService.names.mockResolvedValue({
+    "compute1.resource.org_id1": "compute1.resource.org",
+  });
+  services.GroupResourceProfileService.list.mockResolvedValue([
+    new GroupResourceProfile({
+      groupResourceProfileId: "groupResourceProfileId1",
+      computePreferences: [
+        {
+          computeResourceId: "compute1.resource.org_id1",
+        },
+      ],
+    }),
+  ]);
+  services.ApplicationDeploymentService.list.mockResolvedValue([
+    new ApplicationDeploymentDescription({
+      appDeploymentId: "appDeploymentId1",
+      computeHostId: "compute1.resource.org_id1",
+    }),
+  ]);
+  services.ApplicationDeploymentService.getQueues.mockResolvedValue([
+    new BatchQueue({
+      queueName: "queue2",
+      isDefaultQueue: true,
+      defaultCPUCount: 20,
+      defaultNodeCount: 21,
+      defaultWalltime: 22,
+    }),
+  ]);
+  services.WorkspacePreferencesService.get.mockResolvedValue({
+    most_recent_project_id: "project1",
+  });
+  const experiment = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+    experimentInputs: {
+      appInput1: "value1",
+      appInput2: "value2",
+    },
+  });
+  expect(
+    experiment.experimentInputs.find((i) => i.name === "appInput1").value
+  ).toBe("value1");
+  expect(
+    experiment.experimentInputs.find((i) => i.name === "appInput2").value
+  ).toBe("value2");
+
+  // Don't pass appInput1 and take the default value instead
+  const experiment2 = await createExperiment({
+    applicationName: "test",
+    computeResourceName: "compute1.resource.org",
+    experimentInputs: {
+      // "appInput1": "value1",
+      appInput2: "value2",
+    },
+  });
+  expect(
+    experiment2.experimentInputs.find((i) => i.name === "appInput1").value
+  ).toBe("default1");
+  expect(
+    experiment2.experimentInputs.find((i) => i.name === "appInput2").value
+  ).toBe("value2");
+});