diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml
index 47473dac3..8a43efac0 100644
--- a/.github/workflows/build_and_deploy.yml
+++ b/.github/workflows/build_and_deploy.yml
@@ -1,6 +1,8 @@
name: build and deploy container
on:
pull_request_target:
+ branches:
+ - master
types:
- closed
jobs:
diff --git a/AGENTS.md b/AGENTS.md
index da840452a..3d9eec10c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,12 +2,13 @@
The directory structure of this project is organized as follows:
```
-client/ client js codes
-common/ JS code shared between client and server
-documentMD/ markdown documents
-server/ server js codes
-server/tetst/ server unit test
-test/ end-to-end test with cypress
+client/ client js codes
+common/ JS code shared between client and server
+documentMD/ markdown documents
+server/ server js codes
+server/tetst/ server unit test
+test/cypress/e2e end-to-end test with cypress
+test/cypress/component component test with cypress
```
## npm scripts
diff --git a/client/src/components/componentProperty.vue b/client/src/components/componentProperty.vue
index 56b8d982a..acc84a68c 100644
--- a/client/src/components/componentProperty.vue
+++ b/client/src/components/componentProperty.vue
@@ -105,6 +105,16 @@
variant="outlined"
data-cy="component_property-script-autocomplete"
/>
+
+
{
+ return idx !== this.remoteFileSettingPanelIndex;
+ });
+ }
},
open(newValue) {
//another component is selected while componentProperty is open
diff --git a/documentMD/user_guide/_reference/4_component/01_Task.en.md b/documentMD/user_guide/_reference/4_component/01_Task.en.md
index 629b2ffc1..3332ca0c0 100644
--- a/documentMD/user_guide/_reference/4_component/01_Task.en.md
+++ b/documentMD/user_guide/_reference/4_component/01_Task.en.md
@@ -57,6 +57,36 @@ Therefore, it cannot be changed here.
### submit option
Sets additional options to be specified when the job is submitted.
+### source script
+Specifies an environment configuration file to be loaded before executing the script on the remote host.
+The specified script is loaded using the `source` command.
+
+This is useful when you need to set specific environment variables or use module systems (such as Environment Modules).
+
+Example:
+```bash
+# Contents of setup.sh
+module load intel/2021
+export MY_VAR=value
+```
+
+### checker script
+Specifies a dedicated script to determine the success/failure of the Task component.
+The checker script is executed after the Task component finishes, and its return value (0: success, non-zero: failure) determines the final status of the Task.
+
+When a checker script is set, the return value of the script specified in the script property is ignored, and the checker script's return value is used instead.
+
+Example:
+```bash
+#!/bin/bash
+# Check if output file exists
+if [ -f output.dat ]; then
+ exit 0
+else
+ exit 1
+fi
+```
+
### number of retry
Specifies the number of times the Task component automatically reruns if it fails to run.
If none is specified, the command is not re-executed.
@@ -79,6 +109,30 @@ The expression you enter here is evaluated after the Task component has finished
If both the script name and the javascript expression are not set and you set only the number of retry values, repeat the retry until the script terminates normally or reaches the number of retry settings.
+__Available Environment Variables__
+The following environment variables are available in the script or javascript expression used to determine whether to retry:
+- For shell scripts: `WHEEL_TASK_RT`
+- For javascript expressions: `wheelTaskRT`
+
+These variables contain the return value of the script specified in the script property.
+However, if the checker property is set, the return value of the checker script takes precedence.
+
+Example: Retry only if the return value is 10 (shell script)
+```bash
+#!/bin/bash
+if [ "$WHEEL_TASK_RT" = "10" ]; then
+ exit 0
+else
+ exit 1
+fi
+```
+
+Example: Retry only if the return value is less than 5 (javascript expression)
+```javascript
+wheelTaskRT < 5
+```
+{: .notice--info}
+
### include, exclude

diff --git a/documentMD/user_guide/_reference/4_component/01_Task.md b/documentMD/user_guide/_reference/4_component/01_Task.md
index 8946a1f69..e441fdd8a 100644
--- a/documentMD/user_guide/_reference/4_component/01_Task.md
+++ b/documentMD/user_guide/_reference/4_component/01_Task.md
@@ -57,6 +57,36 @@ use job schedulerを有効にしたときのみ、次のqueue, submit optionプ
### submit option
ジョブ投入時に追加で指定するオプションを設定します。
+### source script
+リモートホストでスクリプトを実行する前に読み込む環境設定ファイルを指定します。
+ここで指定したスクリプトは`source`コマンドで読み込まれます。
+
+例えば、特定の環境変数を設定したり、モジュールシステム(Environment Modules等)を使用する場合に利用します。
+
+例:
+```bash
+# setup.sh の内容
+module load intel/2021
+export MY_VAR=value
+```
+
+### checker script
+Taskコンポーネントの成功/失敗を判定するための専用スクリプトを指定します。
+checkerスクリプトはTaskコンポーネント実行終了後に実行され、その戻り値(0:成功、0以外:失敗)でTaskの最終的な成否が決定されます。
+
+checkerスクリプトが設定されている場合、scriptプロパティで指定したスクリプトの戻り値は無視され、checkerスクリプトの戻り値が使用されます。
+
+例:
+```bash
+#!/bin/bash
+# 出力ファイルの存在確認
+if [ -f output.dat ]; then
+ exit 0
+else
+ exit 1
+fi
+```
+
### number of retry
Taskコンポーネントの実行に失敗したときに、自動的に再実行する回数を指定します。
無指定時は再実行しません。
@@ -79,6 +109,30 @@ Taskコンポーネントの成功 / 失敗を判定するのにjavascript式を
スクリプト名、javascript式ともに未設定で、number of retryの値のみを設定していた場合は、スクリプトが正常終了するか、retryに設定した回数に達するまで再実行を繰り返します。
+__利用可能な環境変数について__
+再実行の判定を行うスクリプトやjavascript式では、以下の環境変数を利用できます。
+- シェルスクリプトの場合: `WHEEL_TASK_RT`
+- javascript式の場合: `wheelTaskRT`
+
+これらの変数には、scriptプロパティで指定されたスクリプトの戻り値が設定されます。
+ただし、checkerプロパティが設定されている場合は、checkerスクリプトの戻り値が優先して使用されます。
+
+例: シェルスクリプトで戻り値が10の場合のみ再実行する
+```bash
+#!/bin/bash
+if [ "$WHEEL_TASK_RT" = "10" ]; then
+ exit 0
+else
+ exit 1
+fi
+```
+
+例: javascript式で戻り値が5未満の場合のみ再実行する
+```javascript
+wheelTaskRT < 5
+```
+{: .notice--info}
+
### include, exclude

diff --git a/server/app/core/dispatcher.js b/server/app/core/dispatcher.js
index e53ae07ff..8dce839f7 100644
--- a/server/app/core/dispatcher.js
+++ b/server/app/core/dispatcher.js
@@ -16,6 +16,7 @@ import { exec } from "./executer.js";
import { getDateString, writeJsonWrapper } from "../lib/utility.js";
import { sanitizePath, convertPathSep, replacePathsep } from "./pathUtils.js";
import { readJsonGreedy } from "./fileUtils.js";
+import { addX } from "./fileUtils.js";
import { deliverFile, deliverFilesOnRemote, deliverFilesFromRemote, deliverFilesFromHPCISS } from "./deliverFile.js";
import { paramVecGenerator, getParamSize, getFilenames, getParamSpacev2 } from "./parameterParser.js";
import { isLocal } from "../../../common/checkComponent.js";
@@ -524,6 +525,14 @@ class Dispatcher extends EventEmitter {
this.setEnv(component);
component.parentType = this.cwfJson.type;
+ //Add execute permission to script and checker
+ const scriptPath = path.resolve(component.workingDir, component.script);
+ await addX(scriptPath);
+ if (component.checker) {
+ const checkerPath = path.resolve(component.workingDir, component.checker);
+ await addX(checkerPath);
+ }
+
exec(component).catch((e)=>{
logWarn(this.projectRootDir, `${this.cwfDir}/${component.name}`, "failed. rt=", component.rt);
logTrace(this.projectRootDir, component.workingDir, "failed due to", e);
diff --git a/server/app/core/executerManager.js b/server/app/core/executerManager.js
index 103251ab0..4dc78ea48 100644
--- a/server/app/core/executerManager.js
+++ b/server/app/core/executerManager.js
@@ -46,6 +46,18 @@ function isExceededLimit(JS, rt, outputText) {
return false;
}
+/**
+ * make source command for remote execution
+ * @param {object} task - task component instance
+ * @returns {string} - source command string
+ */
+function source(task) {
+ if (typeof task.sourceScript === "undefined" || task.sourceScript === null || task.sourceScript.length === 0) {
+ return "";
+ }
+ return `&& source ${task.sourceScript}`;
+}
+
/**
* convert env object to cmandline string
* @param {object} task - task component instance
@@ -128,7 +140,12 @@ function makeBulkOpt(task) {
async function decideFinishState(task) {
let rt = false;
try {
- rt = await _internal.evalCondition(task.projectRootDir, task.condition, task.workingDir, task.currentIndex);
+ const env = Object.assign({}, task.env || {});
+ //Use checkerRt if available, otherwise use task.rt
+ const effectiveRt = (task.checkerRt !== null && typeof task.checkerRt !== "undefined") ? task.checkerRt : task.rt;
+ env.WHEEL_TASK_RT = effectiveRt;
+ env.wheelTaskRT = effectiveRt;
+ rt = await _internal.evalCondition(task.projectRootDir, task.condition, task.workingDir, env);
} catch {
loggerWrapper.logInfo(task.projectRootDir, task.workingDir, "manualFinishCondition is set but exception occurred while evaluting it.");
return false;
@@ -137,11 +154,43 @@ async function decideFinishState(task) {
}
/**
- * determine if task needs to be re-executed
+ * run checker script to determine task state
* @param {object} task - task component instance
- * @returns {boolean} -
+ * @returns {number} - exit code of checker script
*/
-async function needsRetry(task) {
+async function runChecker(task) {
+ if (!task.checker) {
+ return null;
+ }
+
+ const onRemote = task.remotehostID !== "localhost";
+
+ if (onRemote) {
+ const ssh = getSsh(task.projectRootDir, task.remotehostID);
+ const cmd = `cd ${task.remoteWorkingDir} && ./${task.checker}`;
+ loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "exec checker (remote)", cmd);
+
+ const rt = await ssh.exec(cmd, 0, (data)=>{
+ loggerWrapper.logSSHout(task.projectRootDir, task.workingDir, data);
+ });
+ loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "checker (remote) done. rt =", rt);
+ return rt;
+ } else {
+ const script = path.resolve(task.workingDir, task.checker);
+
+ const options = {
+ cwd: task.workingDir,
+ env: Object.assign({}, process.env, task.env),
+ shell: true
+ };
+
+ loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "exec checker (local)", script);
+ const rt = await promisifiedSpawn(task, script, options);
+ loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "checker (local) done. rt =", rt);
+ return rt;
+ }
+}
+async function needsRetry(task, checkerRt) {
if ((typeof task.retry === "undefined" || task.retryCondition === null)
&& (typeof task.retryCondition === "undefined" || task.retryCondition === null)) {
return false;
@@ -151,7 +200,16 @@ async function needsRetry(task) {
return Number.isInteger(task.retry) && task.retry > 0;
}
try {
- rt = await _internal.evalCondition(task.projectRootDir, task.retryCondition, task.workingDir, task.currentIndex);
+ const env = Object.assign({}, task.env || {});
+
+ //Use checkerRt if available, otherwise use task.rt
+ const actualCheckerRt = checkerRt !== undefined ? checkerRt : task.checkerRt;
+ const effectiveRt = (actualCheckerRt !== null && typeof actualCheckerRt !== "undefined") ? actualCheckerRt : task.rt;
+
+ env.WHEEL_TASK_RT = effectiveRt;
+ env.wheelTaskRT = effectiveRt;
+
+ rt = await _internal.evalCondition(task.projectRootDir, task.retryCondition, task.workingDir, env);
} catch {
loggerWrapper.logInfo(task.projectRootDir, task.workingDir, "retryCondition is set but exception occurred while evaluting it. so give up retring");
return false;
@@ -190,9 +248,22 @@ class Executer {
//record job finished time
task.endTime = getDateString(true, true);
+ //run checker script if specified
+ let checkerRt = null;
+ if (task.checker) {
+ try {
+ checkerRt = await runChecker(task);
+ task.checkerRt = checkerRt;
+ } catch (e) {
+ loggerWrapper.logWarn(task.projectRootDir, task.workingDir, "checker script execution failed", e);
+ }
+ }
+
//update task status
let state;
- if (task.manualFinishCondition) {
+ if (task.checker && checkerRt !== null) {
+ state = checkerRt === 0 ? "finished" : "failed";
+ } else if (task.manualFinishCondition) {
state = await decideFinishState(task) ? "finished" : "failed";
} else {
state = task.rt === 0 ? "finished" : "failed";
@@ -200,7 +271,7 @@ class Executer {
await setTaskState(task, state);
//exec useualy returns task.state but to use it in retry function
//to use task in retry function, exec() will be rejected with task object if failed
- if (state === "failed" && await needsRetry(task)) {
+ if (state === "failed" && await needsRetry(task, checkerRt)) {
return Promise.reject(task);
}
return state;
@@ -273,7 +344,7 @@ class RemoteJobExecuter extends Executer {
async exec(task) {
const hostinfo = _internal.getSshHostinfo(task.projectRootDir, task.remotehostID);
const submitOpt = task.submitOption ? task.submitOption : "";
- const submitCmd = `cd ${task.remoteWorkingDir} && ${makeEnv(task)} ${this.JS.submit} ${makeQueueOpt(task, this.JS, this.queues)} ${makeStepOpt(task)} ${makeBulkOpt(task)} ${submitOpt} ./${task.script}`;
+ const submitCmd = `cd ${task.remoteWorkingDir} ${source(task)} && ${makeEnv(task)} ${this.JS.submit} ${makeQueueOpt(task, this.JS, this.queues)} ${makeStepOpt(task)} ${makeBulkOpt(task)} ${submitOpt} ./${task.script}`;
loggerWrapper.logDebug(task.projectRootDir, task.workingDir, "submitting job (remote):", submitCmd);
await setTaskState(task, "running");
const ssh = getSsh(task.projectRootDir, task.remotehostID);
@@ -610,6 +681,7 @@ export {
makeBulkOpt,
decideFinishState,
needsRetry,
+ runChecker,
promisifiedSpawn,
getExecutersKey,
getMaxNumJob,
diff --git a/server/app/core/fileValidator.js b/server/app/core/fileValidator.js
index a6d5d8a18..50dcac00c 100644
--- a/server/app/core/fileValidator.js
+++ b/server/app/core/fileValidator.js
@@ -59,6 +59,40 @@ export async function checkScript(projectRootDir, component) {
return true;
}
+/**
+ * check if checker property has valid value
+ * @param {string} projectRootDir - project's root path
+ * @param {object} component - component which will be tested
+ */
+export async function checkChecker(projectRootDir, component) {
+ if (typeof component.checker !== "string") {
+ return Promise.reject(new Error("checker is not specified"));
+ }
+
+ //Checker must be a filename (not absolute path) under component directory
+ if (path.isAbsolute(component.checker)) {
+ return Promise.reject(new Error("checker must be a filename under component directory, not an absolute path"));
+ }
+
+ //Check if file exists in component directory
+ const componentDir = await _internal.getComponentDir(projectRootDir, component.ID, true);
+ const filename = path.resolve(componentDir, component.checker);
+
+ let stat;
+ try {
+ stat = await fs.stat(filename);
+ } catch (e) {
+ if (e.code !== "ENOENT") {
+ throw e;
+ }
+ return Promise.reject(new Error(`checker is not existing file ${filename}`));
+ }
+ if (!stat.isFile()) {
+ return Promise.reject(new Error(`checker is not file ${filename}`));
+ }
+ return true;
+}
+
/**
* check if parameterFile property has valid value
* @param {string} projectRootDir - project's root path
diff --git a/server/app/core/taskValidator.js b/server/app/core/taskValidator.js
index d270c19d8..01be4b0fe 100644
--- a/server/app/core/taskValidator.js
+++ b/server/app/core/taskValidator.js
@@ -7,7 +7,7 @@
import { isInitialComponent, isLocal } from "./workflowComponent.js";
import { remoteHost } from "../db/db.js";
import { jobScheduler } from "../db/db.js";
-import { checkScript } from "./fileValidator.js";
+import { checkScript, checkChecker } from "./fileValidator.js";
import { validateConditionalCheck } from "./componentResourceValidator.js";
const _internal = {
@@ -48,7 +48,11 @@ export async function validateTask(projectRootDir, component) {
}
}
}
- return checkScript(projectRootDir, component);
+ await checkScript(projectRootDir, component);
+ if (component.checker) {
+ await checkChecker(projectRootDir, component);
+ }
+ return true;
}
/**
diff --git a/server/app/core/workflowComponent.js b/server/app/core/workflowComponent.js
index 4ce5e8281..321b5399f 100644
--- a/server/app/core/workflowComponent.js
+++ b/server/app/core/workflowComponent.js
@@ -179,6 +179,12 @@ class Task extends GeneralComponent {
//if true, project will continue after failing this task.
this.ignoreFailure = false;
+
+ //checker script to determine task success/failure
+ this.checker = null;
+
+ //source script to be loaded before executing on remote host
+ this.sourceScript = null;
}
}
diff --git a/server/app/db/jobScheduler.json b/server/app/db/jobScheduler.json
index aa56eed2a..0860bbc79 100644
--- a/server/app/db/jobScheduler.json
+++ b/server/app/db/jobScheduler.json
@@ -52,7 +52,7 @@
"reSubJobStatusCode": "(?:\\d+\\[\\d+\\]) *(?:CCL|ERR|EXT|RJT) *\\d+ *(\\d+)",
"reExceededLimitError": "PJM 0072 pjsub Job exceeded the accept limit",
"acceptableJobStatus": [
- 0,6
+ 0, 6
]
}
}
diff --git a/server/app/db/jsonSchemas.js b/server/app/db/jsonSchemas.js
index ab9168058..655a9d638 100644
--- a/server/app/db/jsonSchemas.js
+++ b/server/app/db/jsonSchemas.js
@@ -107,12 +107,14 @@ class TaskSchema extends GeneralWorkflowComponentSchema {
this.properties.useJobScheduler = { type: "boolean", default: false };
this.properties.queue = { type: ["string", "null"], default: null };
this.properties.submitOption = { type: ["string", "null"], default: null };
+ this.properties.sourceScript = { type: ["string", "null"], default: null };
this.properties.include = stringArraySchema;
this.properties.exclude = stringArraySchema;
this.properties.state.enum.push(...["stage-in", "waiting", "queued", "stage-out"]);
this.properties.retryCondition = { type: ["string", "null"], default: null };
this.properties.retry = { type: ["number", "null"], default: null };
this.properties.ignoreFailure = { type: "boolean", default: false };
+ this.properties.checker = { type: ["string", "null"], default: null };
}
}
diff --git a/server/app/db/server.json b/server/app/db/server.json
index 7a489bd5e..eac8fdd8d 100644
--- a/server/app/db/server.json
+++ b/server/app/db/server.json
@@ -6,4 +6,4 @@
"withLogin": false,
"userid": null,
"enablePassword": true
-}
\ No newline at end of file
+}
diff --git a/server/app/db/version.json b/server/app/db/version.json
index ff556c900..76f6687ba 100644
--- a/server/app/db/version.json
+++ b/server/app/db/version.json
@@ -1 +1 @@
-{"version": "2026-0213-103108" }
\ No newline at end of file
+{"version": "2026-0224-220307" }
\ No newline at end of file
diff --git a/server/test/app/core/executerManager.js b/server/test/app/core/executerManager.js
index dc2ad91bd..aaf77117c 100644
--- a/server/test/app/core/executerManager.js
+++ b/server/test/app/core/executerManager.js
@@ -292,6 +292,17 @@ describe("UT for executerManager class", function () {
expect(result).to.be.false;
expect(loggerInfoStub).to.have.been.called;
});
+ it("should pass WHEEL_TASK_RT and wheelTaskRT in env to evalCondition", async function () {
+ evalConditionStub.resolves(true);
+ const taskWithRt = { ...mockTask, rt: 42, env: { EXISTING: "value" } };
+ await decideFinishState(taskWithRt);
+ expect(evalConditionStub).to.have.been.calledWith(
+ mockTask.projectRootDir,
+ "mock condition",
+ mockTask.workingDir,
+ sinon.match({ WHEEL_TASK_RT: 42, wheelTaskRT: 42, EXISTING: "value" })
+ );
+ });
});
describe("needsRetry", function () {
const mockTask = {
@@ -354,6 +365,39 @@ describe("UT for executerManager class", function () {
expect(result).to.be.true;
expect(loggerInfoStub).to.have.been.calledWith(mockTask.projectRootDir, mockTask.workingDir, "failed but retring");
});
+ it("should pass WHEEL_TASK_RT and wheelTaskRT in env to evalCondition", async function () {
+ evalConditionStub.resolves(true);
+ const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42, env: { EXISTING: "value" } };
+ await needsRetry(taskWithCondition);
+ expect(evalConditionStub).to.have.been.calledWith(
+ mockTask.projectRootDir,
+ "mock condition",
+ mockTask.workingDir,
+ sinon.match({ WHEEL_TASK_RT: 42, wheelTaskRT: 42, EXISTING: "value" })
+ );
+ });
+ it("should use checker return value as WHEEL_TASK_RT when checkerRt is provided", async function () {
+ evalConditionStub.resolves(true);
+ const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42 };
+ await needsRetry(taskWithCondition, 10);
+ expect(evalConditionStub).to.have.been.calledWith(
+ mockTask.projectRootDir,
+ "mock condition",
+ mockTask.workingDir,
+ sinon.match({ WHEEL_TASK_RT: 10, wheelTaskRT: 10 })
+ );
+ });
+ it("should use task.checkerRt as WHEEL_TASK_RT if checkerRt parameter is undefined", async function () {
+ evalConditionStub.resolves(true);
+ const taskWithCondition = { ...mockTask, retryCondition: "mock condition", rt: 42, checkerRt: 20 };
+ await needsRetry(taskWithCondition);
+ expect(evalConditionStub).to.have.been.calledWith(
+ mockTask.projectRootDir,
+ "mock condition",
+ mockTask.workingDir,
+ sinon.match({ WHEEL_TASK_RT: 20, wheelTaskRT: 20 })
+ );
+ });
});
describe("promisifiedSpawn", function () {
@@ -625,4 +669,62 @@ describe("UT for executerManager class", function () {
);
});
});
+
+ describe("runChecker", ()=>{
+ it("should return null if checker is not specified", async function () {
+ const { runChecker } = await import("../../../app/core/executerManager.js");
+ const task = {
+ checker: null,
+ remotehostID: "localhost",
+ projectRootDir: "/tmp/test",
+ workingDir: "/tmp/test/task"
+ };
+ const result = await runChecker(task);
+ expect(result).to.be.null;
+ });
+
+ it("should execute local checker and return exit code", async function () {
+ const { runChecker } = await import("../../../app/core/executerManager.js");
+ const testDir = path.resolve(testDirRoot, "checkerTest");
+ await fs.ensureDir(testDir);
+ const checkerScript = path.resolve(testDir, "checker.sh");
+ await fs.writeFile(checkerScript, "#!/bin/bash\nexit 0");
+ await fs.chmod(checkerScript, 0o755);
+
+ const task = {
+ checker: "checker.sh",
+ remotehostID: "localhost",
+ projectRootDir: testDir,
+ workingDir: testDir,
+ env: {}
+ };
+
+ const result = await runChecker(task);
+ expect(result).to.equal(0);
+
+ await fs.remove(testDir);
+ });
+
+ it("should execute local checker and return non-zero exit code on failure", async function () {
+ const { runChecker } = await import("../../../app/core/executerManager.js");
+ const testDir = path.resolve(testDirRoot, "checkerTestFail");
+ await fs.ensureDir(testDir);
+ const checkerScript = path.resolve(testDir, "checker.sh");
+ await fs.writeFile(checkerScript, "#!/bin/bash\nexit 1");
+ await fs.chmod(checkerScript, 0o755);
+
+ const task = {
+ checker: "checker.sh",
+ remotehostID: "localhost",
+ projectRootDir: testDir,
+ workingDir: testDir,
+ env: {}
+ };
+
+ const result = await runChecker(task);
+ expect(result).to.equal(1);
+
+ await fs.remove(testDir);
+ });
+ });
});
diff --git a/server/test/app/core/projectController.js b/server/test/app/core/projectController.js
index 0e38bdefe..497219780 100644
--- a/server/test/app/core/projectController.js
+++ b/server/test/app/core/projectController.js
@@ -91,6 +91,38 @@ describe("project Controller UT", function () {
expect(ajv.validate(task0JsonSchema, task0Json)).to.be.true;
expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n10\nundefined");
});
+ it("should use WHEEL_TASK_RT in retryCondition shell script", async ()=>{
+ await updateComponentProperty(projectRootDir, task0.ID, "retry", 2);
+ //Create a retry condition script that checks if WHEEL_TASK_RT equals 10
+ const retryScript = "retry.sh";
+ const retryConditionContent = `${scriptHeader}\nif [ "${referenceEnv("WHEEL_TASK_RT")}" = "10" ]; then\n ${exit(0)}\nelse\n ${exit(1)}\nfi`;
+ await fs.outputFile(path.join(projectRootDir, "task0", retryScript), retryConditionContent);
+ await updateComponentProperty(projectRootDir, task0.ID, "retryCondition", retryScript);
+ await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(10)}`);
+ await runProject(projectRootDir);
+
+ const projectJson = await fs.readJson(path.resolve(projectRootDir, projectJsonFilename));
+ const projectJsonSchema = { required: ["state"], properties: { state: { enum: ["failed"] } } };
+ expect(ajv.validate(projectJsonSchema, projectJson)).to.be.true;
+ const task0Json = await fs.readJson(path.resolve(projectRootDir, "task0", componentJsonFilename));
+ expect(task0Json.state).to.equal("failed");
+ //Verify retry was attempted (statusFile should exist with rt=10)
+ expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n10\nundefined");
+ });
+ it("should use wheelTaskRT in retryCondition javascript expression", async ()=>{
+ await updateComponentProperty(projectRootDir, task0.ID, "retry", 2);
+ //Create a retry condition that checks if wheelTaskRT equals 5
+ await updateComponentProperty(projectRootDir, task0.ID, "retryCondition", "wheelTaskRT === 5");
+ await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(5)}`);
+ await runProject(projectRootDir);
+
+ const projectJson = await fs.readJson(path.resolve(projectRootDir, projectJsonFilename));
+ const projectJsonSchema = { required: ["state"], properties: { state: { enum: ["failed"] } } };
+ expect(ajv.validate(projectJsonSchema, projectJson)).to.be.true;
+ const task0Json = await fs.readJson(path.resolve(projectRootDir, "task0", componentJsonFilename));
+ expect(task0Json.state).to.equal("failed");
+ expect(fs.readFileSync(path.resolve(projectRootDir, "task0", statusFilename), "utf-8")).to.equal("failed\n5\nundefined");
+ });
it("should run project and fail", async ()=>{
await fs.outputFile(path.join(projectRootDir, "task0", scriptName), `${scriptPwd}\n${exit(10)}`);
await runProject(projectRootDir);
diff --git a/server/test/app/core/taskValidator.js b/server/test/app/core/taskValidator.js
index 2cfd0a88e..9461752fa 100644
--- a/server/test/app/core/taskValidator.js
+++ b/server/test/app/core/taskValidator.js
@@ -140,6 +140,55 @@ describe("taskValidator UT", function () {
testTask.submitOption = "-p high -t 10:00";
expect(await validateTask(projectRootDir, testTask)).to.be.true;
});
+
+ it("should be rejected if checker is specified but does not exist", async function () {
+ const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 });
+ testTask.script = "script.sh";
+ const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh");
+ await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'");
+ testTask.checker = "nonexistent_checker.sh";
+ await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker is not existing file/);
+ });
+
+ it("should be rejected if checker is not a file", async function () {
+ const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 });
+ testTask.script = "script.sh";
+ const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh");
+ await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'");
+ testTask.checker = "checker_dir";
+ const checkerDirPath = path.resolve(projectRootDir, testTask.name, "checker_dir");
+ await fs.mkdir(checkerDirPath);
+ await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker is not file/);
+ });
+
+ it("should be resolved with true if checker is specified and exists", async function () {
+ const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 });
+ testTask.script = "script.sh";
+ const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh");
+ await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'");
+ testTask.checker = "checker.sh";
+ const checkerPath = path.resolve(projectRootDir, testTask.name, "checker.sh");
+ await fs.writeFile(checkerPath, "#!/bin/bash\nexit 0");
+ expect(await validateTask(projectRootDir, testTask)).to.be.true;
+ });
+
+ it("should be rejected if checker is an absolute path", async function () {
+ const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 });
+ testTask.script = "script.sh";
+ const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh");
+ await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'");
+ testTask.checker = "/usr/local/bin/checker.sh";
+ await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker must be a filename under component directory/);
+ });
+
+ it("should be rejected if checker is an absolute path on remote host", async function () {
+ const testTask = await createNewComponent(projectRootDir, projectRootDir, "task", { x: 0, y: 0 });
+ testTask.script = "script.sh";
+ const scriptPath = path.resolve(projectRootDir, testTask.name, "script.sh");
+ await fs.writeFile(scriptPath, "#!/bin/bash\necho 'Hello'");
+ testTask.checker = "/home/user/checker.sh";
+ await expect(validateTask(projectRootDir, testTask)).to.be.rejectedWith(/checker must be a filename under component directory/);
+ });
});
describe("validateStepjobTask", ()=>{
diff --git a/test/cypress/e2e/components/bulkjobTask.cy.js b/test/cypress/e2e/components/bulkjobTask.cy.js
index 1246a9181..e8b4bef48 100644
--- a/test/cypress/e2e/components/bulkjobTask.cy.js
+++ b/test/cypress/e2e/components/bulkjobTask.cy.js
@@ -617,6 +617,19 @@ describe("components", ()=>{
cy.confirmDisplayInProperty(DATA_CY_STR, true);
});
+ /**
+ BulkjobTask コンポーネントの基本機能動作確認
+ BulkjobTaskコンポーネント機能確認
+ プロパティ設定確認
+ checker script非表示確認
+ 試験確認内容:checker scriptセレクトボックスが表示されていないことを確認
+ */
+ it("プロパティ設定確認-checker script非表示確認-checker scriptセレクトボックスが表示されていないことを確認", ()=>{
+ cy.createComponent(DEF_COMPONENT_BJ_TASK, BJ_TASK_NAME_0, 501, 500);
+ const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]";
+ cy.get(DATA_CY_STR).should("not.exist");
+ });
+
/**
コンポーネントの基本機能動作確認
BulkjobTaskコンポーネント共通機能確認
diff --git a/test/cypress/e2e/components/stepjobTask.cy.js b/test/cypress/e2e/components/stepjobTask.cy.js
index e55602f10..854047ca8 100644
--- a/test/cypress/e2e/components/stepjobTask.cy.js
+++ b/test/cypress/e2e/components/stepjobTask.cy.js
@@ -468,6 +468,20 @@ describe("components", ()=>{
cy.confirmDisplayInProperty(DATA_CY_STR, true);
});
+ /**
+ StepjobTask コンポーネントの基本機能動作確認
+ StepjobTaskコンポーネント機能確認
+ プロパティ設定確認
+ checker script非表示確認
+ 試験確認内容:checker scriptセレクトボックスが表示されていないことを確認
+ */
+ it("プロパティ設定確認-checker script非表示確認-checker scriptセレクトボックスが表示されていないことを確認", ()=>{
+ cy.createStepjobComponentAndDoubleClick(DEF_COMPONENT_STEPJOB, STEPJOB_NAME_0, 501, 500);
+ cy.createComponent(DEF_COMPONENT_STEPJOB_TASK, STEPJOB_TASK_NAME_0, 501, 500);
+ const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]";
+ cy.get(DATA_CY_STR).should("not.exist");
+ });
+
/**
コンポーネントの基本機能動作確認
StepjobTaskコンポーネント共通機能確認
diff --git a/test/cypress/e2e/components/task.cy.js b/test/cypress/e2e/components/task.cy.js
index 6757ad59b..4b80d5072 100644
--- a/test/cypress/e2e/components/task.cy.js
+++ b/test/cypress/e2e/components/task.cy.js
@@ -617,6 +617,68 @@ describe("components", ()=>{
.should("have.value", "test-a");
});
+ /**
+ Task コンポーネントの基本機能動作確認
+ Taskコンポーネント機能確認
+ プロパティ設定確認
+ checker script表示確認
+ 試験確認内容:checker scriptセレクトボックスが表示されていることを確認
+ */
+ it("プロパティ設定確認-checker script表示確認-checker scriptセレクトボックスが表示されていることを確認", ()=>{
+ const DATA_CY_STR = "[data-cy=\"component_property-checker-autocomplete\"]";
+ cy.confirmDisplayInProperty(DATA_CY_STR, true);
+ });
+
+ /**
+ Task コンポーネントの基本機能動作確認
+ Taskコンポーネント機能確認
+ プロパティ設定確認
+ checker scriptファイル選択表示確認
+ 試験確認内容:checker scriptセレクトボックスで選択したファイルが表示されていることを確認
+ */
+ it("プロパティ設定確認-checker scriptファイル選択表示確認-checker scriptセレクトボックスで選択したファイルが表示されていることを確認", ()=>{
+ cy.createDirOrFile(TYPE_FILE, "test-checker", true);
+ let targetDropBoxCy = "[data-cy=\"component_property-checker-autocomplete\"]";
+ cy.selectValueFromDropdownList(targetDropBoxCy, 3, "test-checker");
+ cy.get("[data-cy=\"component_property-checker-autocomplete\"]").find("input")
+ .should("have.value", "test-checker");
+ });
+
+ /**
+ Task コンポーネントの基本機能動作確認
+ Taskコンポーネント機能確認
+ プロパティ設定確認
+ source script表示確認
+ 試験確認内容:source scriptセレクトボックスが表示されていることを確認
+ */
+ it("プロパティ設定確認-source script表示確認-source scriptセレクトボックスが表示されていることを確認", ()=>{
+ const DATA_CY_STR = "[data-cy=\"component_property-source_script-autocomplete\"]";
+ cy.confirmDisplayInProperty(DATA_CY_STR, true);
+ cy.get(DATA_CY_STR).find("input")
+ .should("be.disabled");
+ });
+
+ /**
+ Task コンポーネントの基本機能動作確認
+ Taskコンポーネント機能確認
+ プロパティ設定確認
+ source scriptファイル選択表示確認
+ 試験確認内容:source scriptセレクトボックスで選択したファイルが表示されていることを確認
+ */
+ it("プロパティ設定確認-source scriptファイル選択表示確認-source scriptセレクトボックスで選択したファイルが表示されていることを確認", ()=>{
+ const SWITCH_CY = "[data-cy=\"component_property-job_scheduler-switch\"]";
+ const FIELD_CY = "[data-cy=\"component_property-source_script-autocomplete\"]";
+ cy.get(SWITCH_CY).click({ force: true });
+ cy.createDirOrFile(TYPE_FILE, "env.sh", true);
+ let targetDropBoxCy = FIELD_CY;
+ cy.selectValueFromDropdownList(targetDropBoxCy, 3, "env.sh");
+ cy.get(FIELD_CY).find("input")
+ .should("have.value", "env.sh");
+ cy.get(SWITCH_CY).click({ force: true });
+ cy.get(FIELD_CY).find("input")
+ .should("be.disabled");
+ });
+
/**
Task コンポーネントの基本機能動作確認
Taskコンポーネント機能確認
diff --git a/test/wheel_config/remotehost.json b/test/wheel_config/remotehost.json
index 482f375c2..fe51488c7 100644
--- a/test/wheel_config/remotehost.json
+++ b/test/wheel_config/remotehost.json
@@ -1,40 +1 @@
-[
- {
- name: "componentTestLabel",
- host: "TestHostname",
- port: 8000,
- user: "testUser",
- numJob: 5,
- jobScheduler: "PBSPro",
- useBulkjob: true,
- useStepjob: true,
- queue: "testQueues",
- sharedHost: "",
- sharedPath: "",
- renewInterval: 0,
- statusCheckInterval: 60,
- maxStatusCheckError: 10,
- rcfile: "/etc/profile",
- useGfarm: true,
- id: "545dd3e0-b26d-11f0-bbe8-272d67e9e112"
- },
- {
- name: "testServer",
- host: "wheel_release_test_server",
- port: 8000,
- numJob: 5,
- queue: "workq",
- jobScheduler: "PBSPro",
- useBulkjob: false,
- useStepjob: false,
- sharedHost: "",
- sharedPath: "",
- renewInterval: 0,
- statusCheckInterval: 60,
- maxStatusCheckError: 10,
- id: "91041660-c8a4-11ee-8b76-87edc1fbf31a",
- user: "testuser",
- rcfile: "/etc/profile",
- prependCmd: ""
- }
-];
+[]
diff --git a/test/wheel_config/server.json b/test/wheel_config/server.json
index 7a489bd5e..eac8fdd8d 100644
--- a/test/wheel_config/server.json
+++ b/test/wheel_config/server.json
@@ -6,4 +6,4 @@
"withLogin": false,
"userid": null,
"enablePassword": true
-}
\ No newline at end of file
+}