/*
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
*
* Copyright (c) 2007 - 2017 ActiveEon
* Contact: contact@activeeon.com
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation: version 3 of
* the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
*/
package functionaltests.workflow;
import static functionaltests.utils.SchedulerTHelper.log;
import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.ow2.proactive.scheduler.common.job.*;
import org.ow2.proactive.scheduler.common.job.factories.JobFactory;
import org.ow2.proactive.scheduler.common.task.*;
import org.ow2.proactive.scheduler.common.task.flow.FlowActionType;
import functionaltests.utils.SchedulerFunctionalTestNonForkedModeNoRestart;
import functionaltests.utils.SchedulerTHelper;
/**
* Tests the correctness of loop / if based workflow-controlled jobs
*
* @author The ProActive Team
* @since ProActive Scheduling 2.2
*/
public abstract class TWorkflowJobs extends SchedulerFunctionalTestNonForkedModeNoRestart {
protected final String jobSuffix = ".xml";
/**
* Each array is a job named $(array index).xml :
* {{ job1 }, { job2 } ... { jobn }}
* Each cell in the matrix is a task and its integer result :
* {{"Task1_name Task1_result", "Task2_name Task2_result", .... "Taskn_name Taskn_result"}}
*
* Each task and result must match exactly once for each job,
* except when using {@link FlowActionType#IF}: there will be no
* result for {@link TaskStatus#SKIPPED} tasks, they must be present in the
* array but with a negative (ie -1) result value
*
* @return the task names / result matrix
*/
public abstract String[][] getJobs();
/**
* @return the search path on the filesystem for job descriptors
*/
public abstract String getJobPrefix();
/**
* For each job described in {@link #data.jobs}, submit the job,
* wait for finished state, and compare expected result for each task with the actual result
*
* @throws Throwable
*/
protected void internalRun() throws Throwable {
String[][] jobs = getJobs();
/*
* Parse job data. The format of an entry is the following (square
* brackets designate optional content):
*
* <task name> <result>[ ([<dependency1>[ <dependency2>[ ...]]])]
*
* i.e. there should be space-separated task name and expected result
* and optionally a list of space-separated list of dependencies in
* parentheses. The order of dependencies is arbitrary. Dependencies
* will be checked only if the list is present.
*
* Examples of valid entries:
*
* "T1 1", "T1 1 ()", "T1 1 ()", "T1 1 (T)", "T2 1 (T0 T1)"
*
*/
for (int i = 0; i < jobs.length; i++) {
Map<String, Long> tasks = new HashMap<>();
for (int j = 0; j < jobs[i].length; j++) {
String[] val = jobs[i][j].split(" ");
try {
tasks.put(val[0], Long.parseLong(val[1]));
} catch (Throwable t) {
System.out.println(jobs[i][j]);
t.printStackTrace();
}
}
// parse dependences info, if present
Map<String, Set<String>> dependences = new HashMap<>();
for (int j = 0; j < jobs[i].length; j++) {
String[] val = jobs[i][j].split(" ", 3);
if (val.length == 3) {
try {
String deps = val[2].substring(1, val[2].length() - 1);
dependences.put(val[0], new HashSet<>(Arrays.asList(deps.split(" "))));
} catch (Throwable t) {
throw new RuntimeException("Error parsing dependencies for entry: " + jobs[i][j], t);
}
}
}
String path = new File(TWorkflowJobs.class.getResource(getJobPrefix() + (i + 1) + jobSuffix)
.toURI()).getAbsolutePath();
log("Testing job: " + path);
testJob(path, tasks, dependences);
}
}
/**
* See @{link {@link SchedulerTHelper#testJobSubmission(Job)}; does about the same,
* but skips the part that expects the initial submitted task set to be identical to
* the finished task set.
*
* @param jobToSubmit
* @param skip tasks that will be skipped due to {@link FlowActionType#IF}, do not wait for their completion
* @return
* @throws Exception
*/
public static JobId testJobSubmission(SchedulerTHelper schedulerHelper, Job jobToSubmit, List<String> skip)
throws Exception {
JobId id = schedulerHelper.submitJob(jobToSubmit);
log("Job submitted, id " + id.toString());
log("Waiting for jobSubmitted");
JobState receivedstate = schedulerHelper.waitForEventJobSubmitted(id);
Assert.assertEquals(id, receivedstate.getId());
log("Waiting for job running");
JobInfo jInfo = schedulerHelper.waitForEventJobRunning(id);
Assert.assertEquals(jInfo.getJobId(), id);
Assert.assertEquals(JobStatus.RUNNING, jInfo.getStatus());
if (jobToSubmit instanceof TaskFlowJob) {
for (Task t : ((TaskFlowJob) jobToSubmit).getTasks()) {
if (skip != null && skip.contains(t.getName())) {
continue;
}
TaskInfo ti = schedulerHelper.waitForEventTaskRunning(id, t.getName());
Assert.assertEquals(t.getName(), ti.getTaskId().getReadableName());
Assert.assertEquals(TaskStatus.RUNNING, ti.getStatus());
}
for (Task t : ((TaskFlowJob) jobToSubmit).getTasks()) {
if (skip != null && skip.contains(t.getName())) {
continue;
}
TaskInfo ti = schedulerHelper.waitForEventTaskFinished(id, t.getName());
Assert.assertEquals(t.getName(), ti.getTaskId().getReadableName());
Assert.assertTrue(TaskStatus.FINISHED.equals(ti.getStatus()));
}
}
log("Waiting for job finished");
jInfo = schedulerHelper.waitForEventJobFinished(id);
Assert.assertEquals(JobStatus.FINISHED, jInfo.getStatus());
log("Job finished");
return id;
}
/**
* Submits the job located at <code>jobPath</code>, compares its results with the ones provided
* in <code>expectedResults</code>
*
* Each task for the provided jobs is a org.ow2.proactive.scheduler.examples.IncrementJob
* which adds all the parameters result + 1.
* Testing each task's result enforces that all expected tasks are present, with the right dependency graph,
* thus the correctness of the job.
*
* @param jobPath
* @param expectedResults
* @param expectedDependences
* @throws Throwable
*/
public void testJob(String jobPath, Map<String, Long> expectedResults, Map<String, Set<String>> expectedDependences)
throws Throwable {
List<String> skip = new ArrayList<>();
for (Entry<String, Long> er : expectedResults.entrySet()) {
if (er.getValue() < 0) {
skip.add(er.getKey());
}
}
Job jobToTest = JobFactory.getFactory().createJob(jobPath);
JobId id = testJobSubmission(schedulerHelper, jobToTest, skip);
JobResult res = schedulerHelper.getJobResult(id);
Assert.assertFalse(schedulerHelper.getJobResult(id).hadException());
compareResults(jobPath, expectedResults, res);
JobState js = schedulerHelper.getSchedulerInterface().getJobState(id);
compareDependences(js, expectedDependences);
schedulerHelper.removeJob(id);
schedulerHelper.waitForEventJobRemoved(id);
}
public void compareResults(String prefix, Map<String, Long> expectedResults, JobResult jobResult) throws Throwable {
for (Entry<String, TaskResult> result : jobResult.getAllResults().entrySet()) {
Long expected = expectedResults.get(result.getKey());
Assert.assertNotNull(prefix + ": Not expecting result for task '" + result.getKey() + "'", expected);
Assert.assertTrue("Task " + result.getKey() + " should be skipped, but returned a result", expected >= 0);
if (!(result.getValue().value() instanceof Long)) {
System.out.println(result.getValue().value() + " " + result.getValue().value().getClass());
}
Assert.assertTrue(prefix + ": Result for task '" + result.getKey() + "' is not an Long",
result.getValue().value() instanceof Long);
Assert.assertEquals(prefix + ": Invalid result for task '" + result.getKey() + "'",
expected,
(Long) result.getValue().value());
}
int skipped = 0;
// tasks SKIPPED are short-circuited by an IF flow action
// they are still in the tasks list, but do not return a result
for (Entry<String, Long> expected : expectedResults.entrySet()) {
if (expected.getValue() < 0) {
Assert.assertFalse("Task " + expected.getKey() + " should be skipped, but returned a result",
jobResult.getAllResults().containsKey(expected.getKey()));
skipped++;
}
}
Assert.assertEquals("Expected and actual result sets are not identical in " + prefix + " (skipped " + skipped +
"): ", expectedResults.size(), jobResult.getAllResults().size() + skipped);
}
public void compareDependences(JobState js, Map<String, Set<String>> expectedDependences) {
for (TaskState ts : js.getTasks()) {
String taskName = ts.getId().getReadableName();
if (expectedDependences.containsKey(taskName)) {
// expected dependences for this task
Set<String> expected = expectedDependences.get(taskName);
// actual dependences of this task
List<TaskState> actualDepTasks = ts.getDependences();
Set<String> actual = new HashSet<>();
if (actualDepTasks != null && actualDepTasks.size() != 0) {
for (TaskState d : actualDepTasks) {
actual.add(d.getId().getReadableName());
}
} else {
actual.add("");
}
// compare expected to actual
Assert.assertEquals("Dependence mismatch for task " + taskName, expected, actual);
}
}
}
}