OOZIE-2685 Test new LauncherAM

Change-Id: Ibe9aefd33ed2a0e50939dfebbf27b66a489f5c95


Project: http://git-wip-us.apache.org/repos/asf/oozie/repo
Commit: http://git-wip-us.apache.org/repos/asf/oozie/commit/050ba570
Tree: http://git-wip-us.apache.org/repos/asf/oozie/tree/050ba570
Diff: http://git-wip-us.apache.org/repos/asf/oozie/diff/050ba570

Branch: refs/heads/oya
Commit: 050ba570b1c3a36c1ab1896fa5ed7ed4b44bc800
Parents: 9151f4e
Author: Peter Bacsko <[email protected]>
Authored: Mon Oct 17 17:08:57 2016 +0200
Committer: Peter Bacsko <[email protected]>
Committed: Mon Oct 17 17:08:57 2016 +0200

----------------------------------------------------------------------
 pom.xml                                         |  14 +-
 sharelib/oozie/pom.xml                          |  11 +
 .../action/hadoop/LauncherAMTestMainClass.java  |  47 ++
 .../oozie/action/hadoop/TestHdfsOperations.java | 121 +++++
 .../oozie/action/hadoop/TestLauncherAM.java     | 541 +++++++++++++++++++
 5 files changed, 733 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/oozie/blob/050ba570/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index ef22b39..ce0bbde 100644
--- a/pom.xml
+++ b/pom.xml
@@ -270,7 +270,7 @@
             <dependency>
                 <groupId>junit</groupId>
                 <artifactId>junit</artifactId>
-                <version>4.10</version>
+                <version>4.11</version>
                 <scope>test</scope>
             </dependency>
 
@@ -1318,6 +1318,18 @@
             </dependency>
 
             <dependency>
+                <groupId>org.mockito</groupId>
+                <artifactId>mockito-core</artifactId>
+                <version>1.10.19</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.hamcrest</groupId>
+                <artifactId>hamcrest-all</artifactId>
+                <version>1.3</version>
+           </dependency>
+
+            <dependency>
                 <groupId>org.powermock</groupId>
                 <artifactId>powermock-core</artifactId>
                 <version>1.6.4</version>

http://git-wip-us.apache.org/repos/asf/oozie/blob/050ba570/sharelib/oozie/pom.xml
----------------------------------------------------------------------
diff --git a/sharelib/oozie/pom.xml b/sharelib/oozie/pom.xml
index 3ea10a5..708371c 100644
--- a/sharelib/oozie/pom.xml
+++ b/sharelib/oozie/pom.xml
@@ -60,6 +60,17 @@
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+       <dependency>
+           <groupId>org.hamcrest</groupId>
+           <artifactId>hamcrest-all</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

http://git-wip-us.apache.org/repos/asf/oozie/blob/050ba570/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/LauncherAMTestMainClass.java
----------------------------------------------------------------------
diff --git 
a/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/LauncherAMTestMainClass.java
 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/LauncherAMTestMainClass.java
new file mode 100644
index 0000000..8752684
--- /dev/null
+++ 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/LauncherAMTestMainClass.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.oozie.action.hadoop;
+
+public class LauncherAMTestMainClass {
+    public static final String SECURITY_EXCEPTION = "security";
+    public static final String LAUNCHER_EXCEPTION = "launcher";
+    public static final String JAVA_EXCEPTION = "java";
+    public static final String THROWABLE = "throwable";
+
+    public static final String JAVA_EXCEPTION_MESSAGE = "Java Exception";
+    public static final String SECURITY_EXCEPTION_MESSAGE = "Security 
Exception";
+    public static final String THROWABLE_MESSAGE = "Throwable";
+    public static final int LAUNCHER_ERROR_CODE = 1234;
+
+    public static void main(String args[]) throws Throwable {
+        System.out.println("Invocation of TestMain");
+
+        if (args != null && args.length > 0) {
+            if (args[0].equals(JAVA_EXCEPTION)) {
+                throw new JavaMainException(new 
RuntimeException(JAVA_EXCEPTION_MESSAGE));
+            } else if (args[0].equals(LAUNCHER_EXCEPTION)) {
+                throw new LauncherMainException(LAUNCHER_ERROR_CODE);
+            } else if (args[0].equals(SECURITY_EXCEPTION)) {
+                throw new SecurityException(SECURITY_EXCEPTION_MESSAGE);
+            } else if (args[0].equals(THROWABLE)) {
+                throw new Throwable(THROWABLE_MESSAGE);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/oozie/blob/050ba570/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestHdfsOperations.java
----------------------------------------------------------------------
diff --git 
a/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestHdfsOperations.java
 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestHdfsOperations.java
new file mode 100644
index 0000000..3a60f63
--- /dev/null
+++ 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestHdfsOperations.java
@@ -0,0 +1,121 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.oozie.action.hadoop;
+
+import static org.junit.Assert.assertEquals;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willThrow;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.io.SequenceFile;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestHdfsOperations {
+    @Mock
+    private SequenceFileWriterFactory seqFileWriterFactoryMock;
+
+    @Mock
+    private SequenceFile.Writer writerMock;
+
+    @Mock
+    private UserGroupInformation ugiMock;
+
+    @Mock
+    private Configuration configurationMock;
+
+    private Path path = new Path(".");
+
+    private Map<String, String> actionData = new HashMap<>();
+
+    @InjectMocks
+    private HdfsOperations hdfsOperations;
+
+    @Before
+    public void setup() throws IOException {
+        configureMocksForHappyPath();
+    }
+
+    @Test
+    public void testActionDataUploadToHdfsSucceeds() throws IOException {
+        configureMocksForHappyPath();
+        actionData.put("testKey", "testValue");
+
+        hdfsOperations.uploadActionDataToHDFS(configurationMock, path, 
actionData);
+
+        
verify(seqFileWriterFactoryMock).createSequenceFileWriter(eq(configurationMock),
+                any(Path.class), eq(Text.class), eq(Text.class));
+        ArgumentCaptor<Text> keyCaptor = ArgumentCaptor.forClass(Text.class);
+        ArgumentCaptor<Text> valueCaptor = ArgumentCaptor.forClass(Text.class);
+        verify(writerMock).append(keyCaptor.capture(), valueCaptor.capture());
+        assertEquals("testKey", keyCaptor.getValue().toString());
+        assertEquals("testValue", valueCaptor.getValue().toString());
+    }
+
+    @Test(expected = IOException.class)
+    public void testActionDataUploadToHdfsFailsWhenAppendingToWriter() throws 
IOException {
+        configureMocksForHappyPath();
+        willThrow(new IOException()).given(writerMock).append(any(Text.class), 
any(Text.class));
+
+        hdfsOperations.uploadActionDataToHDFS(configurationMock, path, 
actionData);
+    }
+
+    @Test(expected = IOException.class)
+    public void testActionDataUploadToHdfsFailsWhenWriterIsNull() throws 
IOException {
+        configureMocksForHappyPath();
+        actionData.put("testKey", "testValue");
+        
given(seqFileWriterFactoryMock.createSequenceFileWriter(eq(configurationMock),
+                any(Path.class), eq(Text.class), 
eq(Text.class))).willReturn(null);
+
+        hdfsOperations.uploadActionDataToHDFS(configurationMock, path, 
actionData);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void configureMocksForHappyPath() throws IOException {
+        given(ugiMock.doAs(any(PrivilegedAction.class))).willAnswer(new 
Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable 
{
+                PrivilegedAction<?> action = (PrivilegedAction<?>) 
invocation.getArguments()[0];
+                return action.run();
+            }
+        });
+
+        
given(seqFileWriterFactoryMock.createSequenceFileWriter(eq(configurationMock),
+                any(Path.class), eq(Text.class), 
eq(Text.class))).willReturn(writerMock);
+    }
+}

http://git-wip-us.apache.org/repos/asf/oozie/blob/050ba570/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestLauncherAM.java
----------------------------------------------------------------------
diff --git 
a/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestLauncherAM.java
 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestLauncherAM.java
new file mode 100644
index 0000000..30441ea
--- /dev/null
+++ 
b/sharelib/oozie/src/test/java/org/apache/oozie/action/hadoop/TestLauncherAM.java
@@ -0,0 +1,541 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.oozie.action.hadoop;
+
+import static 
org.apache.oozie.action.hadoop.LauncherAM.ACTIONOUTPUTTYPE_EXT_CHILD_ID;
+import static 
org.apache.oozie.action.hadoop.LauncherAM.ACTIONOUTPUTTYPE_ID_SWAP;
+import static 
org.apache.oozie.action.hadoop.LauncherAM.ACTIONOUTPUTTYPE_OUTPUT;
+import static org.apache.oozie.action.hadoop.LauncherAM.ACTIONOUTPUTTYPE_STATS;
+import static 
org.apache.oozie.action.hadoop.LauncherAM.ACTION_DATA_EXTERNAL_CHILD_IDS;
+import static org.apache.oozie.action.hadoop.LauncherAM.ACTION_DATA_NEW_ID;
+import static 
org.apache.oozie.action.hadoop.LauncherAM.ACTION_DATA_OUTPUT_PROPS;
+import static org.apache.oozie.action.hadoop.LauncherAM.ACTION_DATA_STATS;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.JAVA_EXCEPTION;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.JAVA_EXCEPTION_MESSAGE;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.LAUNCHER_ERROR_CODE;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.LAUNCHER_EXCEPTION;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.SECURITY_EXCEPTION;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.SECURITY_EXCEPTION_MESSAGE;
+import static org.apache.oozie.action.hadoop.LauncherAMTestMainClass.THROWABLE;
+import static 
org.apache.oozie.action.hadoop.LauncherAMTestMainClass.THROWABLE_MESSAGE;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willReturn;
+import static org.mockito.BDDMockito.willThrow;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.PrivilegedAction;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.yarn.api.records.FinalApplicationStatus;
+import org.apache.hadoop.yarn.client.api.async.AMRMClientAsync;
+import org.apache.oozie.action.hadoop.LauncherAM.LauncherSecurityManager;
+import org.apache.oozie.action.hadoop.LauncherAM.OozieActionResult;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestLauncherAM {
+    private static final String ACTIONDATA_ERROR_PROPERTIES = 
"error.properties";
+    private static final String ACTIONDATA_FINAL_STATUS_PROPERTY = 
"final.status";
+    private static final String ERROR_CODE_PROPERTY = "error.code";
+    private static final String EXCEPTION_STACKTRACE_PROPERTY = 
"exception.stacktrace";
+    private static final String EXCEPTION_MESSAGE_PROPERTY = 
"exception.message";
+    private static final String ERROR_REASON_PROPERTY = "error.reason";
+
+    private static final String EMPTY_STRING = "";
+    private static final String EXIT_CODE_1 = "1";
+    private static final String EXIT_CODE_0 = "0";
+    private static final String DUMMY_XML = "<dummy>dummyXml</dummy>";
+
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Mock
+    private UserGroupInformation ugiMock;
+
+    @Mock
+    private AMRMClientAsyncFactory amRMClientAsyncFactoryMock;
+
+    @Mock
+    private AMRMClientAsync<?> amRmAsyncClientMock;
+
+    @Mock
+    private AMRMCallBackHandler callbackHandlerMock;
+
+    @Mock
+    private HdfsOperations fsOperationsMock;
+
+    @Mock
+    private LocalFsOperations localFsOperationsMock;
+
+    @Mock
+    private PrepareActionsHandler prepareHandlerMock;
+
+    @Mock
+    private LauncherAMCallbackNotifierFactory 
launcherCallbackNotifierFactoryMock;
+
+    @Mock
+    private LauncherAMCallbackNotifier launcherCallbackNotifierMock;
+
+    @Mock
+    private LauncherSecurityManager launcherSecurityManagerMock;
+
+    private Configuration launcherJobConfig = new Configuration();
+
+    @InjectMocks
+    private LauncherAM launcherAM;
+
+    private ExpectedFailureDetails failureDetails = new 
ExpectedFailureDetails();
+
+    @Before
+    public void setup() throws IOException {
+        configureMocksForHappyPath();
+    }
+
+    @Test
+    public void testMainIsSuccessfullyInvokedWithActionData() throws Exception 
{
+        setupActionOutputContents();
+
+        executeLauncher();
+
+        verifyZeroInteractions(prepareHandlerMock);
+        assertSuccessfulExecution(OozieActionResult.RUNNING);
+        assertActionOutputDataPresentAndCorrect();
+    }
+
+    @Test
+    public void testMainIsSuccessfullyInvokedWithoutActionData() throws 
Exception {
+        executeLauncher();
+
+        verifyZeroInteractions(prepareHandlerMock);
+        assertSuccessfulExecution(OozieActionResult.SUCCEEDED);
+        assertNoActionOutputData();
+    }
+
+    @Test
+    public void testActionHasPrepareXML() throws Exception {
+        launcherJobConfig.set(LauncherAM.ACTION_PREPARE_XML, DUMMY_XML);
+
+        executeLauncher();
+
+        verify(prepareHandlerMock).prepareAction(eq(DUMMY_XML), 
any(Configuration.class));
+        assertSuccessfulExecution(OozieActionResult.SUCCEEDED);
+    }
+
+    @Test
+    public void testActionHasEmptyPrepareXML() throws Exception {
+        launcherJobConfig.set(LauncherAM.ACTION_PREPARE_XML, EMPTY_STRING);
+
+        executeLauncher();
+
+        verifyZeroInteractions(prepareHandlerMock);
+        assertSuccessfulExecution(OozieActionResult.SUCCEEDED);
+        assertNoActionOutputData();
+    }
+
+    @Test
+    public void testMainIsSuccessfullyInvokedAndAsyncErrorReceived() throws 
Exception {
+        ErrorHolder errorHolder = new ErrorHolder();
+        errorHolder.setErrorCode(6);
+        errorHolder.setErrorMessage("dummy error");
+        errorHolder.setErrorCause(new Exception());
+        given(callbackHandlerMock.getError()).willReturn(errorHolder);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(null)
+                    .expectedErrorCode("6")
+                    .expectedErrorReason("dummy error")
+                    .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testMainClassNotFound() throws Exception {
+        launcherJobConfig.set(LauncherAM.CONF_OOZIE_ACTION_MAIN_CLASS, 
"org.apache.non.existing.Klass");
+
+        executeLauncher();
+
+        
failureDetails.expectedExceptionMessage(ClassNotFoundException.class.getCanonicalName())
+                .expectedErrorCode(EXIT_CODE_0)
+                
.expectedErrorReason(ClassNotFoundException.class.getCanonicalName())
+                .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testLauncherJobConfCannotBeLoaded() throws Exception {
+        given(localFsOperationsMock.readLauncherConf()).willThrow(new 
RuntimeException());
+        thrown.expect(RuntimeException.class);
+
+        try {
+            executeLauncher();
+        } finally {
+            failureDetails.expectedExceptionMessage(null)
+                .expectedErrorCode(EXIT_CODE_0)
+                .expectedErrorReason("Could not load the Launcher AM 
configuration file")
+                .withStackTrace();
+
+            assertFailedExecution();
+        }
+    }
+
+    @Test
+    public void testActionPrepareFails() throws Exception {
+        launcherJobConfig.set(LauncherAM.ACTION_PREPARE_XML, DUMMY_XML);
+        willThrow(new 
IOException()).given(prepareHandlerMock).prepareAction(anyString(), 
any(Configuration.class));
+        thrown.expect(IOException.class);
+
+        try {
+            executeLauncher();
+        } finally {
+            failureDetails.expectedExceptionMessage(null)
+                .expectedErrorCode(EXIT_CODE_0)
+                .expectedErrorReason("Prepare execution in the Launcher AM has 
failed")
+                .withStackTrace();
+
+            assertFailedExecution();
+        }
+    }
+
+    @Test
+    public void testActionThrowsJavaMainException() throws Exception {
+        setupArgsForMainClass(JAVA_EXCEPTION);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(JAVA_EXCEPTION_MESSAGE)
+            .expectedErrorCode(EXIT_CODE_0)
+            .expectedErrorReason(JAVA_EXCEPTION_MESSAGE)
+            .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testActionThrowsLauncherException() throws Exception {
+        setupArgsForMainClass(LAUNCHER_EXCEPTION);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(null)
+            .expectedErrorCode(String.valueOf(LAUNCHER_ERROR_CODE))
+            .expectedErrorReason("exit code [" + LAUNCHER_ERROR_CODE + "]")
+            .withoutStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testActionThrowsSecurityExceptionWithExitCode0() throws 
Exception {
+        setupArgsForMainClass(SECURITY_EXCEPTION);
+        given(launcherSecurityManagerMock.getExitInvoked()).willReturn(true);
+        given(launcherSecurityManagerMock.getExitCode()).willReturn(0);
+
+        executeLauncher();
+
+        assertSuccessfulExecution(OozieActionResult.SUCCEEDED);
+    }
+
+    @Test
+    public void testActionThrowsSecurityExceptionWithExitCode1() throws 
Exception {
+        setupArgsForMainClass(SECURITY_EXCEPTION);
+        given(launcherSecurityManagerMock.getExitInvoked()).willReturn(true);
+        given(launcherSecurityManagerMock.getExitCode()).willReturn(1);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(null)
+            .expectedErrorCode(EXIT_CODE_1)
+            .expectedErrorReason("exit code ["+ EXIT_CODE_1 + "]")
+            .withoutStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testActionThrowsSecurityExceptionWithoutSystemExit() throws 
Exception {
+        setupArgsForMainClass(SECURITY_EXCEPTION);
+        given(launcherSecurityManagerMock.getExitInvoked()).willReturn(false);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(SECURITY_EXCEPTION_MESSAGE)
+            .expectedErrorCode(EXIT_CODE_0)
+            .expectedErrorReason(SECURITY_EXCEPTION_MESSAGE)
+            .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testActionThrowsThrowable() throws Exception {
+        setupArgsForMainClass(THROWABLE);
+
+        executeLauncher();
+
+        failureDetails.expectedExceptionMessage(THROWABLE_MESSAGE)
+            .expectedErrorCode(EXIT_CODE_0)
+            .expectedErrorReason(THROWABLE_MESSAGE)
+            .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testActionThrowsThrowableAndAsyncErrorReceived() throws 
Exception {
+        setupArgsForMainClass(THROWABLE);
+        ErrorHolder errorHolder = new ErrorHolder();
+        errorHolder.setErrorCode(6);
+        errorHolder.setErrorMessage("dummy error");
+        errorHolder.setErrorCause(new Exception());
+        given(callbackHandlerMock.getError()).willReturn(errorHolder);
+
+        executeLauncher();
+
+        // sync problem overrides async problem
+        failureDetails.expectedExceptionMessage(THROWABLE_MESSAGE)
+            .expectedErrorCode(EXIT_CODE_0)
+            .expectedErrorReason(THROWABLE_MESSAGE)
+            .withStackTrace();
+
+        assertFailedExecution();
+    }
+
+    @Test
+    public void testYarnUnregisterFails() throws Exception {
+        willThrow(new 
IOException()).given(amRmAsyncClientMock).unregisterApplicationMaster(any(FinalApplicationStatus.class),
+                anyString(), anyString());
+        thrown.expect(IOException.class);
+
+        try {
+            executeLauncher();
+        } finally {
+            // TODO: check if this behaviour is correct (url callback: 
successful, but unregister fails)
+            assertSuccessfulExecution(OozieActionResult.SUCCEEDED);
+        }
+    }
+
+    @Test
+    public void testUpdateActionDataFailsWithActionError() throws Exception {
+        setupActionOutputContents();
+        
given(localFsOperationsMock.getLocalFileContentAsString(any(File.class), 
eq(ACTIONOUTPUTTYPE_EXT_CHILD_ID), anyInt()))
+            .willThrow(new IOException());
+        thrown.expect(IOException.class);
+
+        try {
+            executeLauncher();
+        } finally {
+            Map<String, String> actionData = launcherAM.getActionData();
+            assertThat(actionData, 
not(hasKey(ACTION_DATA_EXTERNAL_CHILD_IDS)));
+            
verify(launcherCallbackNotifierMock).notifyURL(OozieActionResult.FAILED);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void configureMocksForHappyPath() throws IOException {
+        launcherJobConfig.set(LauncherAM.OOZIE_ACTION_DIR_PATH, "dummy");
+        launcherJobConfig.set(LauncherAM.OOZIE_JOB_ID, "dummy");
+        launcherJobConfig.set(LauncherAM.OOZIE_ACTION_ID, "dummy");
+        launcherJobConfig.set(LauncherAM.CONF_OOZIE_ACTION_MAIN_CLASS, 
LauncherAMTestMainClass.class.getCanonicalName());
+
+        
given(localFsOperationsMock.readLauncherConf()).willReturn(launcherJobConfig);
+        
given(localFsOperationsMock.fileExists(any(File.class))).willReturn(true);
+
+        
willReturn(amRmAsyncClientMock).given(amRMClientAsyncFactoryMock).createAMRMClientAsync(anyInt());
+        given(ugiMock.doAs(any(PrivilegedAction.class))).willAnswer(new 
Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable 
{
+                PrivilegedAction<?> action = (PrivilegedAction<?>) 
invocation.getArguments()[0];
+                return action.run();
+            }
+        });
+        
given(launcherCallbackNotifierFactoryMock.createCallbackNotifier(any(Configuration.class))).willReturn(launcherCallbackNotifierMock);
+    }
+
+    private void setupActionOutputContents() throws IOException {
+        // output files generated by an action
+        
given(localFsOperationsMock.getLocalFileContentAsString(any(File.class), 
eq(ACTIONOUTPUTTYPE_EXT_CHILD_ID), 
anyInt())).willReturn(ACTIONOUTPUTTYPE_EXT_CHILD_ID);
+        
given(localFsOperationsMock.getLocalFileContentAsString(any(File.class), 
eq(ACTIONOUTPUTTYPE_ID_SWAP), anyInt())).willReturn(ACTIONOUTPUTTYPE_ID_SWAP);
+        
given(localFsOperationsMock.getLocalFileContentAsString(any(File.class), 
eq(ACTIONOUTPUTTYPE_OUTPUT), anyInt())).willReturn(ACTIONOUTPUTTYPE_OUTPUT);
+        
given(localFsOperationsMock.getLocalFileContentAsString(any(File.class), 
eq(ACTIONOUTPUTTYPE_STATS), anyInt())).willReturn(ACTIONOUTPUTTYPE_STATS);
+    }
+
+    private void setupArgsForMainClass(final String...  args) {
+        
launcherJobConfig.set(String.valueOf(LauncherAM.CONF_OOZIE_ACTION_MAIN_ARG_COUNT),
 String.valueOf(args.length));
+
+        for (int i = 0; i < args.length; i++) {
+            
launcherJobConfig.set(String.valueOf(LauncherAM.CONF_OOZIE_ACTION_MAIN_ARG_PREFIX
 + i), args[i]);
+        }
+    }
+
+    private void executeLauncher() throws Exception {
+        launcherAM.run();
+    }
+
+    @SuppressWarnings("unchecked")
+    private void assertSuccessfulExecution(OozieActionResult actionResult) 
throws Exception {
+        verify(amRmAsyncClientMock).registerApplicationMaster(anyString(), 
anyInt(), anyString());
+        
verify(amRmAsyncClientMock).unregisterApplicationMaster(FinalApplicationStatus.SUCCEEDED,
 EMPTY_STRING, EMPTY_STRING);
+        verify(amRmAsyncClientMock).stop();
+        verify(ugiMock, times(2)).doAs(any(PrivilegedAction.class)); // 
prepare & action main
+        
verify(fsOperationsMock).uploadActionDataToHDFS(any(Configuration.class), 
any(Path.class), any(Map.class));
+        
verify(launcherCallbackNotifierFactoryMock).createCallbackNotifier(any(Configuration.class));
+        verify(launcherCallbackNotifierMock).notifyURL(actionResult);
+
+        Map<String, String> actionData = launcherAM.getActionData();
+        verifyFinalStatus(actionData, actionResult);
+        verifyNoError(actionData);
+    }
+
+    private void assertActionOutputDataPresentAndCorrect() {
+        Map<String, String> actionData = launcherAM.getActionData();
+        String extChildId = actionData.get(ACTION_DATA_EXTERNAL_CHILD_IDS);
+        String stats = actionData.get(ACTION_DATA_STATS);
+        String output = actionData.get(ACTION_DATA_OUTPUT_PROPS);
+        String idSwap = actionData.get(ACTION_DATA_NEW_ID);
+
+        assertThat("extChildID output", ACTIONOUTPUTTYPE_EXT_CHILD_ID, 
equalTo(extChildId));
+        assertThat("stats output", ACTIONOUTPUTTYPE_STATS, equalTo(stats));
+        assertThat("action output", ACTIONOUTPUTTYPE_OUTPUT, equalTo(output));
+        assertThat("idSwap output", ACTIONOUTPUTTYPE_ID_SWAP, equalTo(idSwap));
+    }
+
+    private void assertNoActionOutputData() {
+        Map<String, String> actionData = launcherAM.getActionData();
+        String extChildId = actionData.get(ACTION_DATA_EXTERNAL_CHILD_IDS);
+        String stats = actionData.get(ACTION_DATA_STATS);
+        String output = actionData.get(ACTION_DATA_OUTPUT_PROPS);
+        String idSwap = actionData.get(ACTION_DATA_NEW_ID);
+
+        assertThat("extChildId", extChildId, nullValue());
+        assertThat("stats", stats, nullValue());
+        assertThat("Output", output, nullValue());
+        assertThat("idSwap", idSwap, nullValue());
+    }
+
+    private void assertFailedExecution() throws Exception {
+        Map<String, String> actionData = launcherAM.getActionData();
+        
verify(launcherCallbackNotifierFactoryMock).createCallbackNotifier(any(Configuration.class));
+        
verify(launcherCallbackNotifierMock).notifyURL(OozieActionResult.FAILED);
+        verifyFinalStatus(actionData, OozieActionResult.FAILED);
+
+        // Note: actionData contains properties inside a property, so we have 
to extract them into a new Property object
+        String fullError = actionData.get(ACTIONDATA_ERROR_PROPERTIES);
+        Properties props = new Properties();
+        props.load(new StringReader(fullError));
+
+        String errorReason = props.getProperty(ERROR_REASON_PROPERTY);
+        if (failureDetails.expectedErrorReason != null) {
+            assertThat("errorReason", errorReason, 
containsString(failureDetails.expectedErrorReason));
+        } else {
+            assertThat("errorReason", errorReason, nullValue());
+        }
+
+        String exceptionMessage = 
props.getProperty(EXCEPTION_MESSAGE_PROPERTY);
+        if (failureDetails.expectedExceptionMessage != null) {
+            assertThat("exceptionMessage", exceptionMessage, 
containsString(failureDetails.expectedExceptionMessage));
+        } else {
+            assertThat("exceptionMessage", exceptionMessage, nullValue());
+        }
+
+        String stackTrace = props.getProperty(EXCEPTION_STACKTRACE_PROPERTY);
+        if (failureDetails.hasStackTrace) {
+            assertThat("stackTrace", stackTrace, notNullValue());
+        } else {
+            assertThat("stackTrace", stackTrace, nullValue());
+        }
+
+        String errorCode = props.getProperty(ERROR_CODE_PROPERTY);
+        assertThat("errorCode", errorCode, 
equalTo(failureDetails.expectedErrorCode));
+    }
+
+    private void verifyFinalStatus(Map<String, String> actionData, 
OozieActionResult actionResult) {
+        String finalStatus = actionData.get(ACTIONDATA_FINAL_STATUS_PROPERTY);
+        assertThat("actionResult", actionResult.toString(), 
equalTo(finalStatus));
+    }
+
+    private void verifyNoError(Map<String, String> actionData) {
+        String fullError = actionData.get(ACTIONDATA_ERROR_PROPERTIES);
+        assertThat("error properties", fullError, nullValue());
+    }
+
+    private class ExpectedFailureDetails {
+        String expectedExceptionMessage;
+        String expectedErrorCode;
+        String expectedErrorReason;
+        boolean hasStackTrace;
+
+        public ExpectedFailureDetails expectedExceptionMessage(String 
expectedExceptionMessage) {
+            this.expectedExceptionMessage = expectedExceptionMessage;
+            return this;
+        }
+
+        public ExpectedFailureDetails expectedErrorCode(String 
expectedErrorCode) {
+            this.expectedErrorCode = expectedErrorCode;
+            return this;
+        }
+
+        public ExpectedFailureDetails expectedErrorReason(String 
expectedErrorReason) {
+            this.expectedErrorReason = expectedErrorReason;
+            return this;
+        }
+
+        public ExpectedFailureDetails withStackTrace() {
+            this.hasStackTrace = true;
+            return this;
+        }
+
+        public ExpectedFailureDetails withoutStackTrace() {
+            this.hasStackTrace = false;
+            return this;
+        }
+    }
+}
\ No newline at end of file

Reply via email to