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
