co.cask.cdap.internal.app.services.http.handlers.WorkflowHttpHandlerTest.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.internal.app.services.http.handlers.WorkflowHttpHandlerTest.java

Source

/*
 * Copyright  2015-2016 Cask Data, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package co.cask.cdap.internal.app.services.http.handlers;

import co.cask.cdap.AppWithSchedule;
import co.cask.cdap.AppWithStreamSizeSchedule;
import co.cask.cdap.AppWithWorkflow;
import co.cask.cdap.ConcurrentWorkflowApp;
import co.cask.cdap.ConditionalWorkflowApp;
import co.cask.cdap.PauseResumeWorklowApp;
import co.cask.cdap.WorkflowAppWithErrorRuns;
import co.cask.cdap.WorkflowAppWithFork;
import co.cask.cdap.WorkflowAppWithLocalDatasets;
import co.cask.cdap.WorkflowAppWithScopedParameters;
import co.cask.cdap.WorkflowFailureInForkApp;
import co.cask.cdap.WorkflowTokenTestPutApp;
import co.cask.cdap.api.schedule.ScheduleSpecification;
import co.cask.cdap.api.workflow.WorkflowActionNode;
import co.cask.cdap.api.workflow.WorkflowActionSpecification;
import co.cask.cdap.api.workflow.WorkflowToken;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.utils.Tasks;
import co.cask.cdap.gateway.handlers.WorkflowHttpHandler;
import co.cask.cdap.internal.app.services.http.AppFabricTestBase;
import co.cask.cdap.proto.DatasetSpecificationSummary;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramRunStatus;
import co.cask.cdap.proto.ProgramStatus;
import co.cask.cdap.proto.ProgramType;
import co.cask.cdap.proto.RunRecord;
import co.cask.cdap.proto.ScheduledRuntime;
import co.cask.cdap.proto.StreamProperties;
import co.cask.cdap.proto.WorkflowNodeStateDetail;
import co.cask.cdap.proto.WorkflowTokenDetail;
import co.cask.cdap.proto.WorkflowTokenNodeDetail;
import co.cask.cdap.proto.codec.ScheduleSpecificationCodec;
import co.cask.cdap.proto.codec.WorkflowActionSpecificationCodec;
import co.cask.cdap.proto.codec.WorkflowTokenDetailCodec;
import co.cask.cdap.proto.codec.WorkflowTokenNodeDetailCodec;
import co.cask.cdap.proto.id.Ids;
import co.cask.cdap.proto.id.ProgramId;
import co.cask.cdap.test.XSlowTests;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Tests for {@link WorkflowHttpHandler}
 */
public class WorkflowHttpHandlerTest extends AppFabricTestBase {

    private static final Gson GSON = new GsonBuilder()
            .registerTypeAdapter(ScheduleSpecification.class, new ScheduleSpecificationCodec())
            .registerTypeAdapter(WorkflowActionSpecification.class, new WorkflowActionSpecificationCodec())
            .registerTypeAdapter(WorkflowTokenDetail.class, new WorkflowTokenDetailCodec())
            .registerTypeAdapter(WorkflowTokenNodeDetail.class, new WorkflowTokenNodeDetailCodec()).create();

    private static final Type LIST_WORKFLOWACTIONNODE_TYPE = new TypeToken<List<WorkflowActionNode>>() {
    }.getType();
    private static final Type MAP_STRING_TO_WORKFLOWNODESTATEDETAIL_TYPE = new TypeToken<Map<String, WorkflowNodeStateDetail>>() {
    }.getType();
    private static final Type MAP_STRING_TO_DATASETSPECIFICATIONSUMMARY_TYPE = new TypeToken<Map<String, DatasetSpecificationSummary>>() {
    }.getType();

    private void verifyRunningProgramCount(final Id.Program program, final String runId, final int expected)
            throws Exception {
        Tasks.waitFor(expected, new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return runningProgramCount(program, runId);
            }
        }, 10, TimeUnit.SECONDS);
    }

    private Integer runningProgramCount(Id.Program program, String runId) throws Exception {
        String path = String.format("apps/%s/workflows/%s/runs/%s/current", program.getApplicationId(),
                program.getId(), runId);
        HttpResponse response = doGet(getVersionedAPIPath(path, program.getNamespaceId()));
        if (response.getStatusLine().getStatusCode() == 200) {
            String json = EntityUtils.toString(response.getEntity());
            List<WorkflowActionNode> output = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE);
            return output.size();
        }
        return null;
    }

    private void suspendWorkflow(Id.Program program, String runId, int expectedStatusCode) throws Exception {
        String path = String.format("apps/%s/workflows/%s/runs/%s/suspend", program.getApplicationId(),
                program.getId(), runId);
        HttpResponse response = doPost(
                getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId()));
        Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
    }

    private void setAndTestRuntimeArgs(Id.Program programId, Map<String, String> args) throws Exception {
        HttpResponse response;
        String argString = GSON.toJson(args, new TypeToken<Map<String, String>>() {
        }.getType());
        String path = String.format("apps/%s/workflows/%s/runtimeargs", programId.getApplicationId(),
                programId.getId());
        String versionedRuntimeArgsUrl = getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN,
                programId.getNamespaceId());
        response = doPut(versionedRuntimeArgsUrl, argString);

        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        response = doGet(versionedRuntimeArgsUrl);

        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        String responseEntity = EntityUtils.toString(response.getEntity());
        Map<String, String> argsRead = GSON.fromJson(responseEntity, new TypeToken<Map<String, String>>() {
        }.getType());

        Assert.assertEquals(args.size(), argsRead.size());
    }

    /**
     * Tries to resume a Workflow and expect the call completed with the status.
     */
    private void resumeWorkflow(Id.Program program, String runId, int expectedStatusCode) throws Exception {
        String path = String.format("apps/%s/workflows/%s/runs/%s/resume", program.getApplicationId(),
                program.getId(), runId);
        HttpResponse response = doPost(
                getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, program.getNamespaceId()));
        Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
    }

    private HttpResponse getWorkflowCurrentStatus(Id.Program program, String runId) throws Exception {
        String currentUrl = String.format("apps/%s/workflows/%s/runs/%s/current", program.getApplicationId(),
                program.getId(), runId);
        String versionedUrl = getVersionedAPIPath(currentUrl, Constants.Gateway.API_VERSION_3_TOKEN,
                program.getNamespaceId());
        return doGet(versionedUrl);
    }

    private String createInput(String folderName) throws IOException {
        File inputDir = tmpFolder.newFolder(folderName);

        File inputFile = new File(inputDir.getPath() + "/words.txt");
        try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) {
            writer.write("this text has");
            writer.newLine();
            writer.write("two words text inside");
        }
        return inputDir.getAbsolutePath();
    }

    /**
     * Returns a list of {@link ScheduledRuntime}.
     *
     * @param program the program id
     * @param next if true, fetch the list of future run times. If false, fetch the list of past run times.
     */
    private List<ScheduledRuntime> getScheduledRunTime(Id.Program program, boolean next) throws Exception {
        String nextRunTimeUrl = String.format("apps/%s/workflows/%s/%sruntime", program.getApplicationId(),
                program.getId(), next ? "next" : "previous");
        String versionedUrl = getVersionedAPIPath(nextRunTimeUrl, Constants.Gateway.API_VERSION_3_TOKEN,
                program.getNamespaceId());
        HttpResponse response = doGet(versionedUrl);
        return readResponse(response, new TypeToken<List<ScheduledRuntime>>() {
        }.getType());
    }

    private String getLocalDatasetPath(ProgramId workflowId, String runId) {
        String path = String.format("apps/%s/workflows/%s/runs/%s/localdatasets", workflowId.getApplication(),
                workflowId.getProgram(), runId);

        return getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespace());
    }

    private Map<String, DatasetSpecificationSummary> getWorkflowLocalDatasets(ProgramId workflowId, String runId)
            throws Exception {
        HttpResponse response = doGet(getLocalDatasetPath(workflowId, runId));
        return readResponse(response, MAP_STRING_TO_DATASETSPECIFICATIONSUMMARY_TYPE);
    }

    private void deleteWorkflowLocalDatasets(ProgramId workflowId, String runId) throws Exception {
        doDelete(getLocalDatasetPath(workflowId, runId));
    }

    @Test
    public void testLocalDatasetDeletion() throws Exception {
        String keyValueTableType = "co.cask.cdap.api.dataset.lib.KeyValueTable";
        String filesetType = "co.cask.cdap.api.dataset.lib.FileSet";
        Map<String, String> keyValueTableProperties = ImmutableMap.of("foo", "bar");
        Map<String, String> filesetProperties = ImmutableMap.of("anotherFoo", "anotherBar");

        HttpResponse response = deploy(WorkflowAppWithLocalDatasets.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        File waitFile = new File(tmpFolder.newFolder() + "/wait.file");
        File doneFile = new File(tmpFolder.newFolder() + "/done.file");

        ProgramId workflowId = new ProgramId(TEST_NAMESPACE2, WorkflowAppWithLocalDatasets.NAME,
                ProgramType.WORKFLOW, WorkflowAppWithLocalDatasets.WORKFLOW_NAME);

        startProgram(workflowId.toId(), ImmutableMap.of("wait.file", waitFile.getAbsolutePath(), "done.file",
                doneFile.getAbsolutePath(), "dataset.*.keep.local", "true"));

        while (!waitFile.exists()) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        String runId = getRunIdOfRunningProgram(workflowId.toId());

        doneFile.createNewFile();

        waitState(workflowId.toId(), ProgramStatus.STOPPED.name());

        Map<String, DatasetSpecificationSummary> localDatasetSummaries = getWorkflowLocalDatasets(workflowId,
                runId);
        Assert.assertEquals(2, localDatasetSummaries.size());
        DatasetSpecificationSummary keyValueTableSummary = new DatasetSpecificationSummary("MyTable." + runId,
                keyValueTableType, keyValueTableProperties);
        Assert.assertEquals(keyValueTableSummary, localDatasetSummaries.get("MyTable"));
        DatasetSpecificationSummary filesetSummary = new DatasetSpecificationSummary("MyFile." + runId, filesetType,
                filesetProperties);
        Assert.assertEquals(filesetSummary, localDatasetSummaries.get("MyFile"));

        deleteWorkflowLocalDatasets(workflowId, runId);

        localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId);
        Assert.assertEquals(0, localDatasetSummaries.size());

        waitFile = new File(tmpFolder.newFolder() + "/wait.file");
        doneFile = new File(tmpFolder.newFolder() + "/done.file");

        startProgram(workflowId.toId(), ImmutableMap.of("wait.file", waitFile.getAbsolutePath(), "done.file",
                doneFile.getAbsolutePath(), "dataset.MyTable.keep.local", "true"));

        while (!waitFile.exists()) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        runId = getRunIdOfRunningProgram(workflowId.toId());

        doneFile.createNewFile();

        waitState(workflowId.toId(), ProgramStatus.STOPPED.name());
        localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId);
        Assert.assertEquals(1, localDatasetSummaries.size());
        keyValueTableSummary = new DatasetSpecificationSummary("MyTable." + runId, keyValueTableType,
                keyValueTableProperties);

        Assert.assertEquals(keyValueTableSummary, localDatasetSummaries.get("MyTable"));
        deleteWorkflowLocalDatasets(workflowId, runId);

        localDatasetSummaries = getWorkflowLocalDatasets(workflowId, runId);
        Assert.assertEquals(0, localDatasetSummaries.size());
    }

    @Test
    public void testWorkflowPauseResume() throws Exception {
        String pauseResumeWorkflowApp = "PauseResumeWorkflowApp";
        String pauseResumeWorkflow = "PauseResumeWorkflow";

        // Files used to synchronize between this test and workflow execution
        File firstSimpleActionFile = new File(tmpFolder.newFolder() + "/firstsimpleaction.file");
        File firstSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/firstsimpleaction.file.done");

        File forkedSimpleActionFile = new File(tmpFolder.newFolder() + "/forkedsimpleaction.file");
        File forkedSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/forkedsimpleaction.file.done");

        File anotherForkedSimpleActionFile = new File(tmpFolder.newFolder() + "/anotherforkedsimpleaction.file");
        File anotherForkedSimpleActionDoneFile = new File(
                tmpFolder.newFolder() + "/anotherforkedsimpleaction.file.done");

        File lastSimpleActionFile = new File(tmpFolder.newFolder() + "/lastsimpleaction.file");
        File lastSimpleActionDoneFile = new File(tmpFolder.newFolder() + "/lastsimpleaction.file.done");

        HttpResponse response = deploy(PauseResumeWorklowApp.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, pauseResumeWorkflowApp, ProgramType.WORKFLOW,
                pauseResumeWorkflow);

        Map<String, String> runtimeArguments = Maps.newHashMap();
        runtimeArguments.put("first.simple.action.file", firstSimpleActionFile.getAbsolutePath());
        runtimeArguments.put("first.simple.action.donefile", firstSimpleActionDoneFile.getAbsolutePath());
        runtimeArguments.put("forked.simple.action.file", forkedSimpleActionFile.getAbsolutePath());
        runtimeArguments.put("forked.simple.action.donefile", forkedSimpleActionDoneFile.getAbsolutePath());
        runtimeArguments.put("anotherforked.simple.action.file", anotherForkedSimpleActionFile.getAbsolutePath());
        runtimeArguments.put("anotherforked.simple.action.donefile",
                anotherForkedSimpleActionDoneFile.getAbsolutePath());
        runtimeArguments.put("last.simple.action.file", lastSimpleActionFile.getAbsolutePath());
        runtimeArguments.put("last.simple.action.donefile", lastSimpleActionDoneFile.getAbsolutePath());

        setAndTestRuntimeArgs(programId, runtimeArguments);

        // Start the Workflow
        startProgram(programId, 200);

        // Workflow should be running
        waitState(programId, ProgramStatus.RUNNING.name());

        // Get runid for the running Workflow
        String runId = getRunIdOfRunningProgram(programId);

        while (!firstSimpleActionFile.exists()) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Only one Workflow node should be running
        verifyRunningProgramCount(programId, runId, 1);

        // Suspend the Workflow
        suspendWorkflow(programId, runId, 200);

        // Workflow status hould be SUSPENDED
        waitState(programId, ProgramStatus.STOPPED.name());

        // Meta store information for this Workflow should reflect suspended run
        verifyProgramRuns(programId, "suspended");

        // Suspending the already suspended Workflow should give CONFLICT
        suspendWorkflow(programId, runId, 409);

        // Signal the FirstSimpleAction in the Workflow to continue
        Assert.assertTrue(firstSimpleActionDoneFile.createNewFile());

        // Even if the Workflow is suspended, currently executing action will complete and currently running nodes
        // should be zero
        verifyRunningProgramCount(programId, runId, 0);

        // Verify that Workflow is still suspended
        verifyProgramRuns(programId, "suspended");

        // Resume the execution of the Workflow
        resumeWorkflow(programId, runId, 200);

        // Workflow should be running
        waitState(programId, ProgramStatus.RUNNING.name());

        verifyProgramRuns(programId, "running");

        // Resume on already running Workflow should give conflict
        resumeWorkflow(programId, runId, 409);

        // Wait till fork execution in the Workflow starts
        while (!(forkedSimpleActionFile.exists() && anotherForkedSimpleActionFile.exists())) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Workflow should have 2 nodes running because of the fork
        verifyRunningProgramCount(programId, runId, 2);

        // Suspend the Workflow
        suspendWorkflow(programId, runId, 200);

        // Status of the Workflow should be suspended
        waitState(programId, ProgramStatus.STOPPED.name());

        // Store should reflect the suspended status of the Workflow
        verifyProgramRuns(programId, "suspended");

        // Allow currently executing actions to complete
        Assert.assertTrue(forkedSimpleActionDoneFile.createNewFile());
        Assert.assertTrue(anotherForkedSimpleActionDoneFile.createNewFile());

        // Workflow should have zero actions running
        verifyRunningProgramCount(programId, runId, 0);

        verifyProgramRuns(programId, "suspended");

        Assert.assertTrue(!lastSimpleActionFile.exists());

        resumeWorkflow(programId, runId, 200);

        waitState(programId, ProgramStatus.RUNNING.name());

        while (!lastSimpleActionFile.exists()) {
            TimeUnit.SECONDS.sleep(1);
        }

        verifyRunningProgramCount(programId, runId, 1);

        Assert.assertTrue(lastSimpleActionDoneFile.createNewFile());

        verifyProgramRuns(programId, "completed");

        waitState(programId, ProgramStatus.STOPPED.name());

        suspendWorkflow(programId, runId, 404);

        resumeWorkflow(programId, runId, 404);

    }

    @Category(XSlowTests.class)
    @Test
    public void testMultipleWorkflowInstances() throws Exception {
        String appWithConcurrentWorkflow = ConcurrentWorkflowApp.class.getSimpleName();

        // Files used to synchronize between this test and workflow execution
        File tempDir = tmpFolder.newFolder(appWithConcurrentWorkflow);
        File run1File = new File(tempDir, "concurrentRun1.file");
        File run2File = new File(tempDir, "concurrentRun2.file");
        File run1DoneFile = new File(tempDir, "concurrentRun1.done");
        File run2DoneFile = new File(tempDir, "concurrentRun2.done");

        // create app in default namespace so that v2 and v3 api can be tested in the same test
        String defaultNamespace = Id.Namespace.DEFAULT.getId();
        HttpResponse response = deploy(ConcurrentWorkflowApp.class, Constants.Gateway.API_VERSION_3_TOKEN,
                defaultNamespace);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(Id.Namespace.DEFAULT, appWithConcurrentWorkflow,
                ProgramType.WORKFLOW, ConcurrentWorkflowApp.ConcurrentWorkflow.class.getSimpleName());

        // start run 1
        startProgram(programId, ImmutableMap.of(ConcurrentWorkflowApp.FILE_TO_CREATE_ARG,
                run1File.getAbsolutePath(), ConcurrentWorkflowApp.DONE_FILE_ARG, run1DoneFile.getAbsolutePath()),
                200);
        // start run 2
        startProgram(programId, ImmutableMap.of(ConcurrentWorkflowApp.FILE_TO_CREATE_ARG,
                run2File.getAbsolutePath(), ConcurrentWorkflowApp.DONE_FILE_ARG, run2DoneFile.getAbsolutePath()),
                200);

        while (!(run1File.exists() && run2File.exists())) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        verifyMultipleConcurrentRuns(programId);

        Assert.assertTrue(run1DoneFile.createNewFile());
        Assert.assertTrue(run2DoneFile.createNewFile());

        waitState(programId, ProgramStatus.STOPPED.name());
        // delete the application
        deleteApp(programId.getApplication(), 200, 60, TimeUnit.SECONDS);
    }

    private void verifyMultipleConcurrentRuns(Id.Program workflowId) throws Exception {
        verifyProgramRuns(workflowId, ProgramRunStatus.RUNNING.name(), 1);
        List<RunRecord> historyRuns = getProgramRuns(workflowId, "running");
        Assert.assertEquals(2, historyRuns.size());

        HttpResponse response = getWorkflowCurrentStatus(workflowId, historyRuns.get(0).getPid());
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        String json = EntityUtils.toString(response.getEntity());
        List<WorkflowActionNode> nodes = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE);
        Assert.assertEquals(1, nodes.size());
        Assert.assertEquals(ConcurrentWorkflowApp.SimpleAction.class.getSimpleName(),
                nodes.get(0).getProgram().getProgramName());

        response = getWorkflowCurrentStatus(workflowId, historyRuns.get(1).getPid());
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        json = EntityUtils.toString(response.getEntity());
        nodes = GSON.fromJson(json, LIST_WORKFLOWACTIONNODE_TYPE);
        Assert.assertEquals(1, nodes.size());
        Assert.assertEquals(ConcurrentWorkflowApp.SimpleAction.class.getSimpleName(),
                nodes.get(0).getProgram().getProgramName());
    }

    private void verifyFileExists(final List<File> fileList) throws Exception {
        Tasks.waitFor(true, new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                for (File file : fileList) {
                    if (!file.exists()) {
                        return false;
                    }
                }
                return true;
            }
        }, 180, TimeUnit.SECONDS);
    }

    @Test
    public void testWorkflowForkApp() throws Exception {
        File directory = tmpFolder.newFolder();
        Map<String, String> runtimeArgs = new HashMap<>();

        // Files used to synchronize between this test and workflow execution
        File firstFile = new File(directory, "first.file");
        File firstDoneFile = new File(directory, "first.done");
        runtimeArgs.put("first.file", firstFile.getAbsolutePath());
        runtimeArgs.put("first.donefile", firstDoneFile.getAbsolutePath());

        File branch1File = new File(directory, "branch1.file");
        File branch1DoneFile = new File(directory, "branch1.done");
        runtimeArgs.put("branch1.file", branch1File.getAbsolutePath());
        runtimeArgs.put("branch1.donefile", branch1DoneFile.getAbsolutePath());

        File branch2File = new File(directory, "branch2.file");
        File branch2DoneFile = new File(directory, "branch2.done");
        runtimeArgs.put("branch2.file", branch2File.getAbsolutePath());
        runtimeArgs.put("branch2.donefile", branch2DoneFile.getAbsolutePath());

        HttpResponse response = deploy(WorkflowAppWithFork.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithFork.class.getSimpleName(),
                ProgramType.WORKFLOW, WorkflowAppWithFork.WorkflowWithFork.class.getSimpleName());

        setAndTestRuntimeArgs(programId, runtimeArgs);

        // Start a Workflow
        startProgram(programId);

        // Workflow should be running
        waitState(programId, ProgramStatus.RUNNING.name());

        // Get the runId for the currently running Workflow
        String runId = getRunIdOfRunningProgram(programId);

        // Wait till first action in the Workflow starts executing
        verifyFileExists(Lists.newArrayList(firstFile));

        verifyRunningProgramCount(programId, runId, 1);

        // Stop the Workflow
        stopProgram(programId);

        // Workflow run record should be marked 'killed'
        verifyProgramRuns(programId, "killed");

        // Delete the asset created in the previous run
        Assert.assertTrue(firstFile.delete());

        // Start the Workflow again
        startProgram(programId);

        // Workflow should be running
        waitState(programId, ProgramStatus.RUNNING.name());

        // Get the runId for the currently running Workflow
        String newRunId = getRunIdOfRunningProgram(programId);
        Assert.assertTrue(
                String.format(
                        "Expected a new runId to be generated after starting the workflow for the second time, but "
                                + "found old runId '%s' = new runId '%s'",
                        runId, newRunId),
                !runId.equals(newRunId));

        // Store the new RunId
        runId = newRunId;

        // Wait till first action in the Workflow starts executing
        verifyFileExists(Lists.newArrayList(firstFile));

        verifyRunningProgramCount(programId, runId, 1);

        // Signal the first action to continue
        Assert.assertTrue(firstDoneFile.createNewFile());

        // Wait till fork in the Workflow starts executing
        verifyFileExists(Lists.newArrayList(branch1File, branch2File));

        // Two actions should be running in Workflow as a part of the fork
        verifyRunningProgramCount(programId, runId, 2);

        // Stop the program while in fork
        stopProgram(programId, 200);

        // Wait till the program stop
        waitState(programId, ProgramStatus.STOPPED.name());

        // Current endpoint would return 404
        response = getWorkflowCurrentStatus(programId, runId);
        Assert.assertEquals(404, response.getStatusLine().getStatusCode());

        // Now there should be 2 RunRecord with status killed
        verifyProgramRuns(programId, "killed", 1);

        // Delete the assets generated in the previous run
        Assert.assertTrue(firstFile.delete());
        Assert.assertTrue(firstDoneFile.delete());
        Assert.assertTrue(branch1File.delete());
        Assert.assertTrue(branch2File.delete());

        // Restart the run again
        startProgram(programId);

        // Wait till the Workflow is running
        waitState(programId, ProgramStatus.RUNNING.name());

        // Store the new RunRecord for the currently running run
        runId = getRunIdOfRunningProgram(programId);

        // Wait till first action in the Workflow starts executing
        verifyFileExists(Lists.newArrayList(firstFile));

        verifyRunningProgramCount(programId, runId, 1);

        // Signal the first action to continue
        Assert.assertTrue(firstDoneFile.createNewFile());

        // Wait till fork in the Workflow starts executing
        verifyFileExists(Lists.newArrayList(branch1File, branch2File));

        // Two actions should be running in Workflow as a part of the fork
        verifyRunningProgramCount(programId, runId, 2);

        // Signal the Workflow that execution can be continued
        Assert.assertTrue(branch1DoneFile.createNewFile());
        Assert.assertTrue(branch2DoneFile.createNewFile());

        // Workflow should now have one completed run
        verifyProgramRuns(programId, "completed");
    }

    private String getRunIdOfRunningProgram(final Id.Program programId) throws Exception {
        Tasks.waitFor(1, new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return getProgramRuns(programId, "running").size();
            }
        }, 5, TimeUnit.SECONDS);
        List<RunRecord> historyRuns = getProgramRuns(programId, "running");
        Assert.assertEquals(1, historyRuns.size());
        RunRecord record = historyRuns.get(0);
        return record.getPid();
    }

    private Map<String, WorkflowNodeStateDetail> getWorkflowNodeStates(ProgramId workflowId, String runId)
            throws Exception {
        String path = String.format("apps/%s/workflows/%s/runs/%s/nodes/state", workflowId.getApplication(),
                workflowId.getProgram(), runId);

        path = getVersionedAPIPath(path, Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespace());

        HttpResponse response = doGet(path);
        return readResponse(response, MAP_STRING_TO_WORKFLOWNODESTATEDETAIL_TYPE);
    }

    @Category(XSlowTests.class)
    @Test
    public void testWorkflowScopedArguments() throws Exception {
        String workflowRunIdProperty = "workflowrunid";
        HttpResponse response = deploy(WorkflowAppWithScopedParameters.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);

        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        ProgramId programId = Ids.namespace(TEST_NAMESPACE2).app(WorkflowAppWithScopedParameters.APP_NAME)
                .workflow(WorkflowAppWithScopedParameters.ONE_WORKFLOW);

        Map<String, String> runtimeArguments = Maps.newHashMap();

        runtimeArguments.put("debug", "true");
        runtimeArguments.put("mapreduce.*.debug", "false");
        runtimeArguments.put("mapreduce.OneMR.debug", "true");

        runtimeArguments.put("input.path", createInput("ProgramInput"));
        runtimeArguments.put("mapreduce.OneMR.input.path", createInput("OneMRInput"));
        runtimeArguments.put("mapreduce.OneMR.logical.start.time", "1234567890000");
        runtimeArguments.put("mapreduce.AnotherMR.input.path", createInput("AnotherMRInput"));
        runtimeArguments.put("spark.*.input.path", createInput("SparkInput"));

        runtimeArguments.put("output.path", new File(tmpFolder.newFolder(), "ProgramOutput").getAbsolutePath());
        runtimeArguments.put("mapreduce.OneMR.output.path",
                new File(tmpFolder.newFolder(), "OneMROutput").getAbsolutePath());
        runtimeArguments.put("spark.AnotherSpark.output.path",
                new File(tmpFolder.newFolder(), "AnotherSparkOutput").getAbsolutePath());

        runtimeArguments.put("mapreduce.*.processing.time", "1HR");

        runtimeArguments.put("dataset.Purchase.cache.seconds", "30");
        runtimeArguments.put("dataset.UserProfile.schema.property", "constant");
        runtimeArguments.put("dataset.unknown.dataset", "false");
        runtimeArguments.put("dataset.*.read.timeout", "60");

        setAndTestRuntimeArgs(programId.toId(), runtimeArguments);

        // Start the workflow
        startProgram(programId.toId());
        waitState(programId.toId(), ProgramStatus.RUNNING.name());

        // Wait until we have a run record
        verifyProgramRuns(programId.toId(), "running");
        List<RunRecord> workflowHistoryRuns = getProgramRuns(programId.toId(), "running");
        String workflowRunId = workflowHistoryRuns.get(0).getPid();

        Id.Program mr1ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME,
                ProgramType.MAPREDUCE, WorkflowAppWithScopedParameters.ONE_MR);
        waitState(mr1ProgramId, ProgramStatus.RUNNING.name());
        List<RunRecord> oneMRHistoryRuns = getProgramRuns(mr1ProgramId, "running");

        String expectedMessage = String.format(
                "Cannot stop the program '%s' started by the Workflow run '%s'. " + "Please stop the Workflow.",
                new Id.Run(mr1ProgramId, oneMRHistoryRuns.get(0).getPid()), workflowRunId);
        stopProgram(mr1ProgramId, oneMRHistoryRuns.get(0).getPid(), 400, expectedMessage);

        verifyProgramRuns(programId.toId(), "completed");

        workflowHistoryRuns = getProgramRuns(programId.toId(), "completed");

        oneMRHistoryRuns = getProgramRuns(mr1ProgramId, "completed");

        Id.Program mr2ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME,
                ProgramType.MAPREDUCE, WorkflowAppWithScopedParameters.ANOTHER_MR);

        List<RunRecord> anotherMRHistoryRuns = getProgramRuns(mr2ProgramId, "completed");

        Id.Program spark1ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME,
                ProgramType.SPARK, WorkflowAppWithScopedParameters.ONE_SPARK);

        List<RunRecord> oneSparkHistoryRuns = getProgramRuns(spark1ProgramId, "completed");

        Id.Program spark2ProgramId = Id.Program.from(TEST_NAMESPACE2, WorkflowAppWithScopedParameters.APP_NAME,
                ProgramType.SPARK, WorkflowAppWithScopedParameters.ANOTHER_SPARK);

        List<RunRecord> anotherSparkHistoryRuns = getProgramRuns(spark2ProgramId, "completed");

        Assert.assertEquals(1, workflowHistoryRuns.size());
        Assert.assertEquals(1, oneMRHistoryRuns.size());
        Assert.assertEquals(1, anotherMRHistoryRuns.size());
        Assert.assertEquals(1, oneSparkHistoryRuns.size());
        Assert.assertEquals(1, anotherSparkHistoryRuns.size());

        Map<String, String> workflowRunRecordProperties = workflowHistoryRuns.get(0).getProperties();
        Map<String, String> oneMRRunRecordProperties = oneMRHistoryRuns.get(0).getProperties();
        Map<String, String> anotherMRRunRecordProperties = anotherMRHistoryRuns.get(0).getProperties();
        Map<String, String> oneSparkRunRecordProperties = oneSparkHistoryRuns.get(0).getProperties();
        Map<String, String> anotherSparkRunRecordProperties = anotherSparkHistoryRuns.get(0).getProperties();

        Assert.assertNotNull(oneMRRunRecordProperties.get(workflowRunIdProperty));
        Assert.assertEquals(workflowHistoryRuns.get(0).getPid(),
                oneMRRunRecordProperties.get(workflowRunIdProperty));

        Assert.assertNotNull(anotherMRRunRecordProperties.get(workflowRunIdProperty));
        Assert.assertEquals(workflowHistoryRuns.get(0).getPid(),
                anotherMRRunRecordProperties.get(workflowRunIdProperty));

        Assert.assertNotNull(oneSparkRunRecordProperties.get(workflowRunIdProperty));
        Assert.assertEquals(workflowHistoryRuns.get(0).getPid(),
                oneSparkRunRecordProperties.get(workflowRunIdProperty));

        Assert.assertNotNull(anotherSparkRunRecordProperties.get(workflowRunIdProperty));
        Assert.assertEquals(workflowHistoryRuns.get(0).getPid(),
                anotherSparkRunRecordProperties.get(workflowRunIdProperty));

        Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ONE_MR),
                oneMRHistoryRuns.get(0).getPid());
        Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ONE_SPARK),
                oneSparkHistoryRuns.get(0).getPid());
        Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ANOTHER_MR),
                anotherMRHistoryRuns.get(0).getPid());
        Assert.assertEquals(workflowRunRecordProperties.get(WorkflowAppWithScopedParameters.ANOTHER_SPARK),
                anotherSparkHistoryRuns.get(0).getPid());

        // Get Workflow node states
        Map<String, WorkflowNodeStateDetail> nodeStates = getWorkflowNodeStates(programId,
                workflowHistoryRuns.get(0).getPid());

        Assert.assertNotNull(nodeStates);
        Assert.assertEquals(5, nodeStates.size());
        WorkflowNodeStateDetail mrNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_MR);
        Assert.assertNotNull(mrNodeState);
        Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_MR, mrNodeState.getNodeId());
        Assert.assertEquals(oneMRHistoryRuns.get(0).getPid(), mrNodeState.getRunId());

        mrNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ANOTHER_MR);
        Assert.assertNotNull(mrNodeState);
        Assert.assertEquals(WorkflowAppWithScopedParameters.ANOTHER_MR, mrNodeState.getNodeId());
        Assert.assertEquals(anotherMRHistoryRuns.get(0).getPid(), mrNodeState.getRunId());

        WorkflowNodeStateDetail sparkNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_SPARK);
        Assert.assertNotNull(sparkNodeState);
        Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_SPARK, sparkNodeState.getNodeId());
        Assert.assertEquals(oneSparkHistoryRuns.get(0).getPid(), sparkNodeState.getRunId());

        sparkNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ANOTHER_SPARK);
        Assert.assertNotNull(sparkNodeState);
        Assert.assertEquals(WorkflowAppWithScopedParameters.ANOTHER_SPARK, sparkNodeState.getNodeId());
        Assert.assertEquals(anotherSparkHistoryRuns.get(0).getPid(), sparkNodeState.getRunId());

        WorkflowNodeStateDetail oneActionNodeState = nodeStates.get(WorkflowAppWithScopedParameters.ONE_ACTION);
        Assert.assertNotNull(oneActionNodeState);
        Assert.assertEquals(WorkflowAppWithScopedParameters.ONE_ACTION, oneActionNodeState.getNodeId());
    }

    @Ignore
    @Test
    public void testWorkflowSchedules() throws Exception {
        // Steps for the test:
        // 1. Deploy the app
        // 2. Verify the schedules
        // 3. Verify the history after waiting a while
        // 4. Suspend the schedule
        // 5. Verify there are no runs after the suspend by looking at the history
        // 6. Resume the schedule
        // 7. Verify there are runs after the resume by looking at the history

        String appName = "AppWithSchedule";
        String workflowName = "SampleWorkflow";
        String sampleSchedule = "SampleSchedule";

        // deploy app with schedule in namespace 2
        HttpResponse response = deploy(AppWithSchedule.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName);

        Map<String, String> runtimeArguments = ImmutableMap.of("someKey", "someWorkflowValue", "workflowKey",
                "workflowValue");

        setAndTestRuntimeArgs(programId, runtimeArguments);

        // get schedules
        List<ScheduleSpecification> schedules = getSchedules(TEST_NAMESPACE2, appName, workflowName);
        Assert.assertEquals(1, schedules.size());
        String scheduleName = schedules.get(0).getSchedule().getName();
        Assert.assertFalse(scheduleName.isEmpty());

        // TODO [CDAP-2327] Sagar Investigate why following check fails sometimes. Mostly test case issue.
        // List<ScheduledRuntime> previousRuntimes = getScheduledRunTime(programId, scheduleName, "previousruntime");
        // Assert.assertTrue(previousRuntimes.size() == 0);

        long current = System.currentTimeMillis();

        // Resume the schedule
        Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule));
        // Check schedule status
        assertSchedule(programId, scheduleName, true, 30, TimeUnit.SECONDS);

        List<ScheduledRuntime> runtimes = getScheduledRunTime(programId, true);
        String id = runtimes.get(0).getId();
        Assert.assertTrue(
                String.format("Expected schedule id '%s' to contain schedule name '%s'", id, scheduleName),
                id.contains(scheduleName));
        Long nextRunTime = runtimes.get(0).getTime();
        Assert.assertTrue(String.format("Expected nextRuntime '%s' to be greater than current runtime '%s'",
                nextRunTime, current), nextRunTime > current);

        // Verify that at least one program is completed
        verifyProgramRuns(programId, "completed");

        // Suspend the schedule
        Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName));
        // check paused state
        assertSchedule(programId, scheduleName, false, 30, TimeUnit.SECONDS);

        // check that there were at least 1 previous runs
        List<ScheduledRuntime> previousRuntimes = getScheduledRunTime(programId, false);
        int numRuns = previousRuntimes.size();
        Assert.assertTrue(
                String.format("After sleeping for two seconds, the schedule should have at least triggered "
                        + "once, but found %s previous runs", numRuns),
                numRuns >= 1);

        // Verify no program running
        verifyNoRunWithStatus(programId, "running");

        // get number of completed runs after schedule is suspended
        int workflowRuns = getProgramRuns(programId, "completed").size();

        // verify that resuming the suspended schedule again has expected behavior (spawns new runs)
        Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, scheduleName));
        //check scheduled state
        assertSchedule(programId, scheduleName, true, 30, TimeUnit.SECONDS);

        // Verify that the program ran after the schedule was resumed
        verifyProgramRuns(programId, "completed", workflowRuns);

        // Suspend the schedule
        Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName));
        //check paused state
        assertSchedule(programId, scheduleName, false, 30, TimeUnit.SECONDS);

        //Check status of a non existing schedule
        try {
            assertSchedule(programId, "invalid", true, 2, TimeUnit.SECONDS);
            Assert.fail();
        } catch (Exception e) {
            // expected
        }

        //Schedule operations using invalid namespace
        try {
            assertSchedule(Id.Program.from(TEST_NAMESPACE1, appName, ProgramType.WORKFLOW, workflowName),
                    scheduleName, true, 2, TimeUnit.SECONDS);
            Assert.fail();
        } catch (Exception e) {
            // expected
        }
        Assert.assertEquals(404, suspendSchedule(TEST_NAMESPACE1, appName, scheduleName));
        Assert.assertEquals(404, resumeSchedule(TEST_NAMESPACE1, appName, scheduleName));

        verifyNoRunWithStatus(programId, "running");
        deleteApp(Id.Application.from(TEST_NAMESPACE2, AppWithSchedule.class.getSimpleName()), 200);
    }

    @Test
    public void testStreamSizeSchedules() throws Exception {
        // Steps for the test:
        // 1. Deploy the app
        // 2. Verify the schedules
        // 3. Ingest data in the stream
        // 4. Verify the history after waiting a while
        // 5. Suspend the schedule
        // 6. Ingest data in the stream
        // 7. Verify there are no runs after the suspend by looking at the history
        // 8. Resume the schedule
        // 9. Verify there are runs after the resume by looking at the history

        String appName = "AppWithStreamSizeSchedule";
        String sampleSchedule1 = "SampleSchedule1";
        String sampleSchedule2 = "SampleSchedule2";
        String workflowName = "SampleWorkflow";
        String streamName = "stream";

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName);

        StringBuilder longStringBuilder = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            longStringBuilder.append("dddddddddd");
        }
        String longString = longStringBuilder.toString();

        // deploy app with schedule in namespace 2
        HttpResponse response = deploy(AppWithStreamSizeSchedule.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule1));
        Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, sampleSchedule2));

        // get schedules
        List<ScheduleSpecification> schedules = getSchedules(TEST_NAMESPACE2, appName, workflowName);
        Assert.assertEquals(2, schedules.size());
        String scheduleName1 = schedules.get(0).getSchedule().getName();
        String scheduleName2 = schedules.get(1).getSchedule().getName();
        Assert.assertNotNull(scheduleName1);
        Assert.assertFalse(scheduleName1.isEmpty());

        // Change notification threshold for stream
        response = doPut(String.format("/v3/namespaces/%s/streams/%s/properties", TEST_NAMESPACE2, streamName),
                "{'notification.threshold.mb': 1}");
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        response = doGet(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName));
        String json = EntityUtils.toString(response.getEntity());
        StreamProperties properties = new Gson().fromJson(json, StreamProperties.class);
        Assert.assertEquals(1, properties.getNotificationThresholdMB().intValue());

        // Ingest over 1MB of data in stream
        for (int i = 0; i < 12; ++i) {
            response = doPost(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName),
                    longString);
            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        }

        // Only schedule 1 should get executed
        verifyProgramRuns(programId, "completed");

        //Check schedule status
        assertSchedule(programId, scheduleName1, true, 30, TimeUnit.SECONDS);
        assertSchedule(programId, scheduleName2, true, 30, TimeUnit.SECONDS);

        Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName1));
        Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName2));
        //check paused state
        assertSchedule(programId, scheduleName1, false, 30, TimeUnit.SECONDS);
        assertSchedule(programId, scheduleName2, false, 30, TimeUnit.SECONDS);

        int workflowRuns = getProgramRuns(programId, "completed").size();
        // Should still be one
        Assert.assertEquals(1, workflowRuns);

        // Sleep for some time and verify there are no more scheduled jobs after the suspend.
        for (int i = 0; i < 12; ++i) {
            response = doPost(String.format("/v3/namespaces/%s/streams/%s", TEST_NAMESPACE2, streamName),
                    longString);
            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
        }
        TimeUnit.SECONDS.sleep(5);

        int workflowRunsAfterSuspend = getProgramRuns(programId, "completed").size();
        Assert.assertEquals(workflowRuns, workflowRunsAfterSuspend);

        Assert.assertEquals(200, resumeSchedule(TEST_NAMESPACE2, appName, scheduleName1));

        assertRunHistory(programId, "completed", workflowRunsAfterSuspend, 60, TimeUnit.SECONDS);

        //check scheduled state
        assertSchedule(programId, scheduleName1, true, 30, TimeUnit.SECONDS);

        //Check status of a non existing schedule
        try {
            assertSchedule(programId, "invalid", true, 2, TimeUnit.SECONDS);
            Assert.fail();
        } catch (Exception e) {
            // expected
        }

        Assert.assertEquals(200, suspendSchedule(TEST_NAMESPACE2, appName, scheduleName1));

        //check paused state
        assertSchedule(programId, scheduleName1, false, 30, TimeUnit.SECONDS);

        //Schedule operations using invalid namespace
        try {
            assertSchedule(Id.Program.from(TEST_NAMESPACE1, appName, ProgramType.WORKFLOW, workflowName),
                    scheduleName1, true, 2, TimeUnit.SECONDS);
            Assert.fail();
        } catch (Exception e) {
            // expected
        }
        Assert.assertEquals(404, suspendSchedule(TEST_NAMESPACE1, appName, scheduleName1));
        Assert.assertEquals(404, resumeSchedule(TEST_NAMESPACE1, appName, scheduleName1));

        TimeUnit.SECONDS.sleep(2); //wait till any running jobs just before suspend call completes.
    }

    @Test
    public void testWorkflowRuns() throws Exception {
        String appName = "WorkflowAppWithErrorRuns";
        String workflowName = "WorkflowWithErrorRuns";

        HttpResponse response = deploy(WorkflowAppWithErrorRuns.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, appName, ProgramType.WORKFLOW, workflowName);

        // Test the "KILLED" state of the Workflow.
        File instance1File = new File(tmpFolder.newFolder() + "/instance1.file");
        File instance2File = new File(tmpFolder.newFolder() + "/instance2.file");
        File doneFile = new File(tmpFolder.newFolder() + "/done.file");

        // Start the first Workflow run.
        Map<String, String> propertyMap = ImmutableMap.of("simple.action.file", instance1File.getAbsolutePath(),
                "simple.action.donefile", doneFile.getAbsolutePath());
        startProgram(programId, propertyMap);

        // Start another Workflow run.
        propertyMap = ImmutableMap.of("simple.action.file", instance2File.getAbsolutePath(),
                "simple.action.donefile", doneFile.getAbsolutePath());
        startProgram(programId, propertyMap);

        // Wait till the execution of actions in both Workflow runs is started.
        while (!(instance1File.exists() && instance2File.exists())) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Verify that there are two runs of the Workflow currently running.
        List<RunRecord> historyRuns = getProgramRuns(programId, "running");
        Assert.assertEquals(2, historyRuns.size());

        // Stop both Workflow runs.
        String runId = historyRuns.get(0).getPid();
        stopProgram(programId, runId, 200);
        runId = historyRuns.get(1).getPid();
        stopProgram(programId, runId, 200);

        // Verify both runs should be marked "KILLED".
        verifyProgramRuns(programId, "killed", 1);

        // Test the "COMPLETE" state of the Workflow.
        File instanceFile = new File(tmpFolder.newFolder() + "/instance.file");
        propertyMap = ImmutableMap.of("simple.action.file", instanceFile.getAbsolutePath(),
                "simple.action.donefile", doneFile.getAbsolutePath());
        startProgram(programId, propertyMap);
        while (!instanceFile.exists()) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        // Verify that currently only one run of the Workflow should be running.
        historyRuns = getProgramRuns(programId, "running");
        Assert.assertEquals(1, historyRuns.size());

        Assert.assertTrue(doneFile.createNewFile());

        // Verify that Workflow should move to "COMPLETED" state.
        verifyProgramRuns(programId, "completed");

        // Test the "FAILED" state of the program.
        propertyMap = ImmutableMap.of("ThrowError", "true");
        startProgram(programId, propertyMap);

        // Verify that the Workflow should be marked as "FAILED".
        verifyProgramRuns(programId, "failed");
    }

    private String createConditionInput(String folderName, int numGoodRecords, int numBadRecords)
            throws IOException {
        File inputDir = tmpFolder.newFolder(folderName);

        File inputFile = new File(inputDir.getPath() + "/data.txt");

        try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) {
            // dummy good records containing ":" separated fields
            for (int i = 0; i < numGoodRecords; i++) {
                writer.write("Afname:ALname:A:B");
                writer.newLine();
            }
            // dummy bad records in which fields are not separated by ":"
            for (int i = 0; i < numBadRecords; i++) {
                writer.write("Afname ALname A B");
                writer.newLine();
            }
        }
        return inputDir.getAbsolutePath();
    }

    @Category(XSlowTests.class)
    @Test
    public void testWorkflowCondition() throws Exception {
        String conditionalWorkflowApp = "ConditionalWorkflowApp";
        String conditionalWorkflow = "ConditionalWorkflow";

        HttpResponse response = deploy(ConditionalWorkflowApp.class, Constants.Gateway.API_VERSION_3_TOKEN,
                TEST_NAMESPACE2);

        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Id.Program programId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp, ProgramType.WORKFLOW,
                conditionalWorkflow);

        Map<String, String> runtimeArguments = Maps.newHashMap();

        // Files used to synchronize between this test and workflow execution
        File ifForkOneActionFile = new File(tmpFolder.newFolder() + "/iffork_one.file");
        File ifForkOneActionDoneFile = new File(tmpFolder.newFolder() + "/iffork_one.file.done");
        runtimeArguments.put("iffork_one.simple.action.file", ifForkOneActionFile.getAbsolutePath());
        runtimeArguments.put("iffork_one.simple.action.donefile", ifForkOneActionDoneFile.getAbsolutePath());

        File ifForkAnotherActionFile = new File(tmpFolder.newFolder() + "/iffork_another.file");
        File ifForkAnotherActionDoneFile = new File(tmpFolder.newFolder() + "/iffork_another.file.done");
        runtimeArguments.put("iffork_another.simple.action.file", ifForkAnotherActionFile.getAbsolutePath());
        runtimeArguments.put("iffork_another.simple.action.donefile",
                ifForkAnotherActionDoneFile.getAbsolutePath());

        File elseForkOneActionFile = new File(tmpFolder.newFolder() + "/elsefork_one.file");
        File elseForkOneActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_one.file.done");
        runtimeArguments.put("elsefork_one.simple.action.file", elseForkOneActionFile.getAbsolutePath());
        runtimeArguments.put("elsefork_one.simple.action.donefile", elseForkOneActionDoneFile.getAbsolutePath());

        File elseForkAnotherActionFile = new File(tmpFolder.newFolder() + "/elsefork_another.file");
        File elseForkAnotherActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_another.file.done");
        runtimeArguments.put("elsefork_another.simple.action.file", elseForkAnotherActionFile.getAbsolutePath());
        runtimeArguments.put("elsefork_another.simple.action.donefile",
                elseForkAnotherActionDoneFile.getAbsolutePath());

        File elseForkThirdActionFile = new File(tmpFolder.newFolder() + "/elsefork_third.file");
        File elseForkThirdActionDoneFile = new File(tmpFolder.newFolder() + "/elsefork_third.file.done");
        runtimeArguments.put("elsefork_third.simple.action.file", elseForkThirdActionFile.getAbsolutePath());
        runtimeArguments.put("elsefork_third.simple.action.donefile",
                elseForkThirdActionDoneFile.getAbsolutePath());

        // create input data in which number of good records are lesser than the number of bad records
        runtimeArguments.put("inputPath", createConditionInput("ConditionProgramInput", 2, 12));
        runtimeArguments.put("outputPath",
                new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath());
        setAndTestRuntimeArgs(programId, runtimeArguments);

        // Start the workflow
        startProgram(programId);

        // Since the number of good records are lesser than the number of bad records,
        // 'else' branch of the condition will get executed.
        // Wait till the execution of the fork on the else branch starts
        while (!(elseForkOneActionFile.exists() && elseForkAnotherActionFile.exists()
                && elseForkThirdActionFile.exists())) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Get running program run
        String runId = getRunIdOfRunningProgram(programId);

        // Since the fork on the else branch of condition has 3 parallel branches
        // there should be 3 programs currently running
        verifyRunningProgramCount(programId, runId, 3);

        // Signal the Workflow to continue
        Assert.assertTrue(elseForkOneActionDoneFile.createNewFile());
        Assert.assertTrue(elseForkAnotherActionDoneFile.createNewFile());
        Assert.assertTrue(elseForkThirdActionDoneFile.createNewFile());

        verifyProgramRuns(programId, "completed");

        List<RunRecord> workflowHistoryRuns = getProgramRuns(programId, "completed");

        Id.Program recordVerifierProgramId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp,
                ProgramType.MAPREDUCE, "RecordVerifier");

        List<RunRecord> recordVerifierRuns = getProgramRuns(recordVerifierProgramId, "completed");

        Id.Program wordCountProgramId = Id.Program.from(TEST_NAMESPACE2, conditionalWorkflowApp,
                ProgramType.MAPREDUCE, "ClassicWordCount");

        List<RunRecord> wordCountRuns = getProgramRuns(wordCountProgramId, "completed");

        Assert.assertEquals(1, workflowHistoryRuns.size());
        Assert.assertEquals(1, recordVerifierRuns.size());
        Assert.assertEquals(0, wordCountRuns.size());

        // create input data in which number of good records are greater than the number of bad records
        runtimeArguments.put("inputPath", createConditionInput("AnotherConditionProgramInput", 10, 2));
        runtimeArguments.put("mapreduce.RecordVerifier.outputPath",
                new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath());
        runtimeArguments.put("mapreduce.ClassicWordCount.outputPath",
                new File(tmpFolder.newFolder(), "ConditionProgramOutput").getAbsolutePath());

        setAndTestRuntimeArgs(programId, runtimeArguments);

        // Start the workflow
        startProgram(programId);

        // Since the number of good records are greater than the number of bad records,
        // 'if' branch of the condition will get executed.
        // Wait till the execution of the fork on the if branch starts
        while (!(ifForkOneActionFile.exists() && ifForkAnotherActionFile.exists())) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Get running program run
        runId = getRunIdOfRunningProgram(programId);

        // Since the fork on the if branch of the condition has 2 parallel branches
        // there should be 2 programs currently running
        verifyRunningProgramCount(programId, runId, 2);

        // Signal the Workflow to continue
        Assert.assertTrue(ifForkOneActionDoneFile.createNewFile());
        Assert.assertTrue(ifForkAnotherActionDoneFile.createNewFile());

        verifyProgramRuns(programId, "completed", 1);

        workflowHistoryRuns = getProgramRuns(programId, "completed");
        recordVerifierRuns = getProgramRuns(recordVerifierProgramId, "completed");
        wordCountRuns = getProgramRuns(wordCountProgramId, "completed");

        Assert.assertEquals(2, workflowHistoryRuns.size());
        Assert.assertEquals(2, recordVerifierRuns.size());
        Assert.assertEquals(1, wordCountRuns.size());
    }

    @Test
    @SuppressWarnings("ConstantConditions")
    public void testWorkflowToken() throws Exception {
        Assert.assertEquals(200, deploy(AppWithWorkflow.class).getStatusLine().getStatusCode());
        Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, AppWithWorkflow.NAME);
        final Id.Workflow workflowId = Id.Workflow.from(appId, AppWithWorkflow.SampleWorkflow.NAME);
        String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath();
        startProgram(workflowId, ImmutableMap.of("inputPath", createInput("input"), "outputPath", outputPath));

        Tasks.waitFor(1, new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return getProgramRuns(workflowId, ProgramRunStatus.COMPLETED.name()).size();
            }
        }, 60, TimeUnit.SECONDS);
        List<RunRecord> programRuns = getProgramRuns(workflowId, ProgramRunStatus.COMPLETED.name());
        Assert.assertEquals(1, programRuns.size());
        RunRecord runRecord = programRuns.get(0);
        String pid = runRecord.getPid();
        // Verify entire worfklow token
        WorkflowTokenDetail workflowTokenDetail = getWorkflowToken(workflowId, pid, null, null);
        List<WorkflowTokenDetail.NodeValueDetail> nodeValueDetails = workflowTokenDetail.getTokenData()
                .get(AppWithWorkflow.DummyAction.TOKEN_KEY);
        Assert.assertEquals(2, nodeValueDetails.size());
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.FIRST_ACTION, nodeValueDetails.get(0).getNode());
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.SECOND_ACTION, nodeValueDetails.get(1).getNode());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(0).getValue());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(1).getValue());
        // Verify entire workflow token by passing in the scope and key in the request
        workflowTokenDetail = getWorkflowToken(workflowId, pid, WorkflowToken.Scope.USER,
                AppWithWorkflow.DummyAction.TOKEN_KEY);
        nodeValueDetails = workflowTokenDetail.getTokenData().get(AppWithWorkflow.DummyAction.TOKEN_KEY);
        Assert.assertEquals(2, nodeValueDetails.size());
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.FIRST_ACTION, nodeValueDetails.get(0).getNode());
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.SECOND_ACTION, nodeValueDetails.get(1).getNode());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(0).getValue());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE, nodeValueDetails.get(1).getValue());

        // Get workflow level tokens
        WorkflowTokenNodeDetail nodeDetail = getWorkflowToken(workflowId, pid, AppWithWorkflow.SampleWorkflow.NAME,
                WorkflowToken.Scope.USER, null);
        Map<String, String> tokenData = nodeDetail.getTokenDataAtNode();
        Assert.assertEquals(2, tokenData.size());
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.INITIALIZE_TOKEN_VALUE,
                tokenData.get(AppWithWorkflow.SampleWorkflow.INITIALIZE_TOKEN_KEY));
        Assert.assertEquals(AppWithWorkflow.SampleWorkflow.DESTROY_TOKEN_SUCCESS_VALUE,
                tokenData.get(AppWithWorkflow.SampleWorkflow.DESTROY_TOKEN_KEY));

        // Verify workflow token at a given node
        WorkflowTokenNodeDetail tokenAtNode = getWorkflowToken(workflowId, pid,
                AppWithWorkflow.SampleWorkflow.FIRST_ACTION, null, null);
        Map<String, String> tokenDataAtNode = tokenAtNode.getTokenDataAtNode();
        Assert.assertEquals(1, tokenDataAtNode.size());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE,
                tokenDataAtNode.get(AppWithWorkflow.DummyAction.TOKEN_KEY));
        // Verify workflow token at a given node by passing in a scope and a key
        tokenAtNode = getWorkflowToken(workflowId, pid, AppWithWorkflow.SampleWorkflow.FIRST_ACTION,
                WorkflowToken.Scope.USER, AppWithWorkflow.DummyAction.TOKEN_KEY);
        tokenDataAtNode = tokenAtNode.getTokenDataAtNode();
        Assert.assertEquals(1, tokenDataAtNode.size());
        Assert.assertEquals(AppWithWorkflow.DummyAction.TOKEN_VALUE,
                tokenDataAtNode.get(AppWithWorkflow.DummyAction.TOKEN_KEY));
    }

    private WorkflowTokenDetail getWorkflowToken(Id.Workflow workflowId, String runId,
            @Nullable WorkflowToken.Scope scope, @Nullable String key) throws Exception {
        String workflowTokenUrl = String.format("apps/%s/workflows/%s/runs/%s/token", workflowId.getApplicationId(),
                workflowId.getId(), runId);
        String versionedUrl = getVersionedAPIPath(appendScopeAndKeyToUrl(workflowTokenUrl, scope, key),
                Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespaceId());
        HttpResponse response = doGet(versionedUrl);
        return readResponse(response, new TypeToken<WorkflowTokenDetail>() {
        }.getType(), GSON);
    }

    private WorkflowTokenNodeDetail getWorkflowToken(Id.Workflow workflowId, String runId, String nodeName,
            @Nullable WorkflowToken.Scope scope, @Nullable String key) throws Exception {
        String workflowTokenUrl = String.format("apps/%s/workflows/%s/runs/%s/nodes/%s/token",
                workflowId.getApplicationId(), workflowId.getId(), runId, nodeName);
        String versionedUrl = getVersionedAPIPath(appendScopeAndKeyToUrl(workflowTokenUrl, scope, key),
                Constants.Gateway.API_VERSION_3_TOKEN, workflowId.getNamespaceId());
        HttpResponse response = doGet(versionedUrl);
        return readResponse(response, new TypeToken<WorkflowTokenNodeDetail>() {
        }.getType(), GSON);
    }

    private String appendScopeAndKeyToUrl(String workflowTokenUrl, @Nullable WorkflowToken.Scope scope,
            String key) {
        StringBuilder output = new StringBuilder(workflowTokenUrl);
        if (scope != null) {
            output.append(String.format("?scope=%s", scope.name()));
            if (key != null) {
                output.append(String.format("&key=%s", key));
            }
        } else if (key != null) {
            output.append(String.format("?key=%s", key));
        }
        return output.toString();
    }

    private String createInputForRecordVerification(String folderName) throws IOException {
        File inputDir = tmpFolder.newFolder(folderName);

        File inputFile = new File(inputDir.getPath() + "/words.txt");
        try (BufferedWriter writer = Files.newBufferedWriter(inputFile.toPath(), Charsets.UTF_8)) {
            writer.write("id1:value1");
            writer.newLine();
            writer.write("id2:value2");
            writer.newLine();
            writer.write("id3:value3");
        }
        return inputDir.getAbsolutePath();
    }

    @Test
    public void testWorkflowTokenPut() throws Exception {
        Assert.assertEquals(200, deploy(WorkflowTokenTestPutApp.class).getStatusLine().getStatusCode());
        Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, WorkflowTokenTestPutApp.NAME);
        Id.Workflow workflowId = Id.Workflow.from(appId, WorkflowTokenTestPutApp.WorkflowTokenTestPut.NAME);
        Id.Program mapReduceId = Id.Program.from(appId, ProgramType.MAPREDUCE,
                WorkflowTokenTestPutApp.RecordCounter.NAME);
        Id.Program sparkId = Id.Program.from(appId, ProgramType.SPARK, WorkflowTokenTestPutApp.SparkTestApp.NAME);

        // Start program with inputPath and outputPath arguments.
        // This should succeed. The programs inside the workflow will attempt to write to the workflow token
        // from the Mapper's and Reducer's methods as well as from a Spark closure, and they will throw an exception
        // if that succeeds.
        // The MapReduce's beforeSubmit will record the workflow run id in the token, and the onFinish as well
        // as the mapper and the reducer will validate that they have the same workflow run id.
        String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath();
        startProgram(workflowId, ImmutableMap.of("inputPath", createInputForRecordVerification("sixthInput"),
                "outputPath", outputPath));

        waitState(workflowId, ProgramStatus.RUNNING.name());
        waitState(workflowId, ProgramStatus.STOPPED.name());

        // validate the completed workflow run and validate that it is the same as recorded in the token
        verifyProgramRuns(workflowId, "completed");
        List<RunRecord> runs = getProgramRuns(workflowId, "completed");
        Assert.assertEquals(1, runs.size());
        String wfRunId = runs.get(0).getPid();
        WorkflowTokenDetail tokenDetail = getWorkflowToken(workflowId, wfRunId, null, null);
        List<WorkflowTokenDetail.NodeValueDetail> details = tokenDetail.getTokenData().get("wf.runid");
        Assert.assertEquals(1, details.size());
        Assert.assertEquals(wfRunId, details.get(0).getValue());

        // validate that none of the mapper, reducer or spark closure were able to write to the token
        for (String key : new String[] { "mapper.initialize.key", "map.key", "reducer.initialize.key", "reduce.key",
                "some.key" }) {
            Assert.assertFalse(tokenDetail.getTokenData().containsKey(key));
        }

        List<RunRecord> sparkProgramRuns = getProgramRuns(sparkId, ProgramRunStatus.COMPLETED.name());
        Assert.assertEquals(1, sparkProgramRuns.size());
    }

    @Ignore
    @Test
    public void testWorkflowForkFailure() throws Exception {
        // Deploy an application containing workflow with fork. Fork executes MapReduce programs
        // 'FirstMapReduce' and 'SecondMapReduce' in parallel. Workflow is started with runtime argument
        // "mapreduce.SecondMapReduce.throw.exception", so that the MapReduce program 'SecondMapReduce'
        // fails. This causes the 'FirstMapReduce' program to get killed and Workflow is marked as failed.
        Assert.assertEquals(200, deploy(WorkflowFailureInForkApp.class).getStatusLine().getStatusCode());
        Id.Application appId = Id.Application.from(Id.Namespace.DEFAULT, WorkflowFailureInForkApp.NAME);
        Id.Workflow workflowId = Id.Workflow.from(appId, WorkflowFailureInForkApp.WorkflowWithFailureInFork.NAME);
        Id.Program firstMRId = Id.Program.from(appId, ProgramType.MAPREDUCE,
                WorkflowFailureInForkApp.FIRST_MAPREDUCE_NAME);
        Id.Program secondMRId = Id.Program.from(appId, ProgramType.MAPREDUCE,
                WorkflowFailureInForkApp.SECOND_MAPREDUCE_NAME);

        String outputPath = new File(tmpFolder.newFolder(), "output").getAbsolutePath();
        File fileToSync = new File(tmpFolder.newFolder() + "/sync.file");
        File fileToWait = new File(tmpFolder.newFolder() + "/wait.file");
        startProgram(workflowId,
                ImmutableMap.of("inputPath", createInput("testWorkflowForkFailureInput"), "outputPath", outputPath,
                        "sync.file", fileToSync.getAbsolutePath(), "wait.file", fileToWait.getAbsolutePath(),
                        "mapreduce." + WorkflowFailureInForkApp.SECOND_MAPREDUCE_NAME + ".throw.exception",
                        "true"));
        waitState(workflowId, ProgramStatus.RUNNING.name());
        waitState(workflowId, ProgramStatus.STOPPED.name());

        verifyProgramRuns(workflowId, "failed");

        List<RunRecord> mapReduceProgramRuns = getProgramRuns(firstMRId, ProgramRunStatus.KILLED.name());
        Assert.assertEquals(1, mapReduceProgramRuns.size());

        mapReduceProgramRuns = getProgramRuns(secondMRId, ProgramRunStatus.FAILED.name());
        Assert.assertEquals(1, mapReduceProgramRuns.size());
    }
}