Bug 574432 - Test for modifying eclipse.allowAppRelaunch at runtime

This is hard to test cleanly because relaunching requires the first
application to exit, but exiting the test runner application ends the
test. Work around that by using the test runner as the second
(relaunched) application. This means that if the test fails, it shows
up not as failed but with an error "Test did not run".

Change-Id: I12dc31f42e2d7b4f261610ab0190ed5940d62d55
Signed-off-by: Christian Walther <walther@indel.ch>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.framework/+/182604
Tested-by: Equinox Bot <equinox-bot@eclipse.org>
Reviewed-by: Thomas Watson <tjwatson@us.ibm.com>
diff --git a/bundles/org.eclipse.osgi.tests/plugin.xml b/bundles/org.eclipse.osgi.tests/plugin.xml
index b27d6ae..f8453e1 100644
--- a/bundles/org.eclipse.osgi.tests/plugin.xml
+++ b/bundles/org.eclipse.osgi.tests/plugin.xml
@@ -95,4 +95,16 @@
          </run>
       </application>
    </extension>
+   <extension
+         id="relaunchApp"
+         point="org.eclipse.core.runtime.applications">
+      <application
+            cardinality="1"
+            thread="main"
+            visible="true">
+         <run
+               class="org.eclipse.osgi.tests.appadmin.RelaunchApp">
+         </run>
+      </application>
+   </extension>
 </plugin>
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/AllTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/AllTests.java
index 37a0e72..66f8ca6 100644
--- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/AllTests.java
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/AllTests.java
@@ -20,6 +20,7 @@
 	public static Test suite() {
 		TestSuite suite = new TestSuite(AllTests.class.getName());
 		suite.addTest(ApplicationAdminTest.suite());
+		suite.addTest(ApplicationRelaunchTest.suite());
 		return suite;
 	}
 }
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/ApplicationRelaunchTest.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/ApplicationRelaunchTest.java
new file mode 100644
index 0000000..d91ba9a
--- /dev/null
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/ApplicationRelaunchTest.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2021 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.osgi.tests.appadmin;
+
+import java.util.HashMap;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.eclipse.core.tests.session.ConfigurationSessionTestSuite;
+import org.eclipse.core.tests.session.SetupManager.SetupException;
+import org.eclipse.osgi.tests.OSGiTest;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.application.ApplicationDescriptor;
+import org.osgi.service.application.ApplicationHandle;
+
+// This is for the most part a stripped down copy of ApplicationAdminTest.
+public class ApplicationRelaunchTest extends OSGiTest {
+	public static final String testRunnerRelauncherApp = PI_OSGI_TESTS + ".relaunchApp"; //$NON-NLS-1$
+	public static final String testResults = "test.results"; //$NON-NLS-1$
+	public static final String SUCCESS = "success"; //$NON-NLS-1$
+	public static final String simpleResults = "test.simpleResults"; //$NON-NLS-1$
+	public static final String[] tests = new String[] { "testRelaunch" };
+	private static final String PI_OSGI_SERVICES = "org.eclipse.osgi.services"; //$NON-NLS-1$
+	private static final String PI_OSGI_UTIL = "org.eclipse.osgi.util";
+
+	public static Test suite() {
+		TestSuite suite = new TestSuite(ApplicationRelaunchTest.class.getName());
+
+		ConfigurationSessionTestSuite appAdminSessionTest = new ConfigurationSessionTestSuite(PI_OSGI_TESTS, ApplicationRelaunchTest.class.getName());
+		String[] ids = ConfigurationSessionTestSuite.MINIMAL_BUNDLE_SET;
+		for (String id : ids) {
+			appAdminSessionTest.addBundle(id);
+		}
+		appAdminSessionTest.addBundle(PI_OSGI_UTIL);
+		appAdminSessionTest.addBundle(PI_OSGI_SERVICES);
+		appAdminSessionTest.addBundle(PI_OSGI_TESTS);
+		appAdminSessionTest.setApplicationId(testRunnerRelauncherApp);
+		try {
+			appAdminSessionTest.getSetup().setSystemProperty("eclipse.application.registerDescriptors", "true"); //$NON-NLS-1$//$NON-NLS-2$
+		} catch (SetupException e) {
+			throw new RuntimeException(e);
+		}
+		// we add tests the hard way so we can control the order of the tests.
+		for (String test : tests) {
+			appAdminSessionTest.addTest(new ApplicationRelaunchTest(test));
+		}
+		suite.addTest(appAdminSessionTest);
+		return suite;
+	}
+
+	public ApplicationRelaunchTest(String name) {
+		super(name);
+	}
+
+	private ApplicationDescriptor getApplication(String appName) {
+		try {
+			BundleContext context = getContext();
+			assertNotNull("BundleContext is null!!", context); //$NON-NLS-1$
+			Class appDescClass = ApplicationDescriptor.class;
+			assertNotNull("ApplicationDescriptor.class is null!!", appDescClass); //$NON-NLS-1$
+			ServiceReference[] refs = context.getServiceReferences(appDescClass.getName(), "(" + ApplicationDescriptor.APPLICATION_PID + "=" + appName + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			if (refs == null || refs.length == 0) {
+				refs = getContext().getServiceReferences(ApplicationDescriptor.class.getName(), null);
+				String availableApps = ""; //$NON-NLS-1$
+				if (refs != null) {
+					for (int i = 0; i < refs.length; i++) {
+						availableApps += refs[i].getProperty(ApplicationDescriptor.APPLICATION_PID);
+						if (i < refs.length - 1)
+							availableApps += ","; //$NON-NLS-1$
+					}
+				}
+				fail("Could not find app pid: " + appName + " available apps are: " + availableApps); //$NON-NLS-1$ //$NON-NLS-2$
+			}
+			ApplicationDescriptor result = (ApplicationDescriptor) getContext().getService(refs[0]);
+			if (result != null)
+				getContext().ungetService(refs[0]);
+			else
+				fail("Could not get application descriptor service: " + appName); //$NON-NLS-1$
+			return result;
+		} catch (InvalidSyntaxException e) {
+			fail("Could not create app filter", e); //$NON-NLS-1$
+		}
+		return null;
+	}
+
+	private HashMap getArguments() {
+		HashMap args = new HashMap();
+		args.put(testResults, new HashMap());
+		return args;
+	}
+
+	public void testRelaunch() {
+		// this is the same as ApplicationAdminTest.testSimpleApp() (but launched
+		// through a different test runner app RelaunchApp which is the thing being
+		// tested)
+		ApplicationDescriptor app = getApplication(PI_OSGI_TESTS + ".simpleApp"); //$NON-NLS-1$
+		HashMap args = getArguments();
+		HashMap results = (HashMap) args.get(testResults);
+		try {
+			ApplicationHandle handle = app.launch(args);
+			handle.destroy();
+		} catch (Throwable e) {
+			fail("failed to launch simpleApp", e); //$NON-NLS-1$
+		}
+		String result = (String) results.get(simpleResults);
+		assertEquals("Check application result", SUCCESS, result); //$NON-NLS-1$
+	}
+
+}
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/RelaunchApp.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/RelaunchApp.java
new file mode 100644
index 0000000..f349f77
--- /dev/null
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/appadmin/RelaunchApp.java
@@ -0,0 +1,136 @@
+/*******************************************************************************
+* Copyright (c) 2021 Indel AG and others.
+* 
+* This program and the accompanying materials
+* are made available under the terms of the Eclipse Public License 2.0
+* which accompanies this distribution, and is available at
+* https://www.eclipse.org/legal/epl-2.0/
+*
+* SPDX-License-Identifier: EPL-2.0
+* 
+* Contributors:
+*     Indel AG - initial API and implementation
+*******************************************************************************/
+package org.eclipse.osgi.tests.appadmin;
+
+import java.util.Collection;
+import java.util.Map;
+import org.eclipse.equinox.app.IApplication;
+import org.eclipse.equinox.app.IApplicationContext;
+import org.eclipse.osgi.service.environment.EnvironmentInfo;
+import org.eclipse.osgi.tests.OSGiTestsActivator;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.application.ApplicationDescriptor;
+import org.osgi.service.application.ApplicationException;
+import org.osgi.service.application.ApplicationHandle;
+
+public class RelaunchApp implements IApplication {
+
+	@Override
+	public Object start(IApplicationContext context) throws Exception {
+		final Map arguments = context.getArguments();
+
+		// Setting eclipse.allowAppRelaunch to true at runtime should allow us to launch
+		// multiple applications in sequence
+		ServiceReference<EnvironmentInfo> envref = OSGiTestsActivator.getContext()
+				.getServiceReference(EnvironmentInfo.class);
+		EnvironmentInfo env = OSGiTestsActivator.getContext().getService(envref);
+		if (Boolean.valueOf(env.getProperty("eclipse.allowAppRelaunch"))) { //$NON-NLS-1$
+			throw new AssertionError("eclipse.allowAppRelaunch should not be set initially"); //$NON-NLS-1$
+		}
+		env.setProperty("eclipse.allowAppRelaunch", "true"); //$NON-NLS-1$ //$NON-NLS-2$
+		OSGiTestsActivator.getContext().ungetService(envref);
+
+		// Get a handle for the running application so we can wait for it to exit
+		ServiceReference<ApplicationHandle> thisAppRef = OSGiTestsActivator.getContext()
+				.getServiceReference(ApplicationHandle.class);
+		ApplicationHandle thisAppHandle = OSGiTestsActivator.getContext().getService(thisAppRef);
+
+		new Thread("launcher") { //$NON-NLS-1$
+			public void run() {
+				// Wait for this application to exit
+				try {
+					thisAppHandle.getExitValue(0);
+				} catch (ApplicationException e) {
+					// does not occur for timeout 0
+				} catch (InterruptedException e) {
+					// I don't think this should occur
+					e.printStackTrace();
+				}
+
+				// Get the descriptor for the actual test runner application.
+				// Need a test runner that runs in the main thread to avoid race conditions.
+				Collection<ServiceReference<ApplicationDescriptor>> testAppRefs = null;
+				try {
+					testAppRefs = OSGiTestsActivator.getContext().getServiceReferences(
+							org.osgi.service.application.ApplicationDescriptor.class,
+							"(" + Constants.SERVICE_PID + "=org.eclipse.pde.junit.runtime.nonuithreadtestapplication)"); //$NON-NLS-1$ //$NON-NLS-2$
+				} catch (InvalidSyntaxException e) {
+					// shouldn't happen, the hardcoded filter expression
+					// should be syntactically correct
+					e.printStackTrace();
+				}
+				ServiceReference<ApplicationDescriptor> testAppRef = testAppRefs.iterator().next();
+				ApplicationDescriptor testAppDescriptor = OSGiTestsActivator.getContext().getService(testAppRef);
+
+				// Launch the new application
+				// If it does launch, it will run some unrelated succeeding test
+				// and thereby confirm that relaunching works.
+				try {
+					ApplicationHandle testAppHandle;
+					// There is a race condition in that the previous
+					// application may not have exited far enough yet for
+					// the EclipseAppLauncher to allow launching of a new
+					// application: Setting the exit value happens earlier
+					// in EclipseAppLauncher.runApplication() (inside
+					// EclipseAppHandle.run()) than releasing runningLock.
+					// Unfortunately there is no way to wait for the
+					// EclipseAppLauncher to be ready, so just try again
+					// after a delay when that happens.
+					while (true) {
+						try {
+							testAppHandle = testAppDescriptor.launch(arguments);
+							break;
+						} catch (IllegalStateException e) {
+							Thread.sleep(100);
+						}
+					}
+
+					// Wait for the test application to exit
+					testAppHandle.getExitValue(0);
+				} catch (ApplicationException | InterruptedException e) {
+					// ApplicationException "The main thread is not available to launch the
+					// application" can happen when the test fails
+					e.printStackTrace();
+				} finally {
+					OSGiTestsActivator.getContext().ungetService(thisAppRef);
+					OSGiTestsActivator.getContext().ungetService(testAppRef);
+
+					try {
+						// This will not return but cause the process to terminate
+						OSGiTestsActivator.getContext().getBundle(0).stop();
+					} catch (BundleException e) {
+						e.printStackTrace();
+					}
+				}
+			}
+		}.start();
+
+		// If relaunching does not work, the process will end after this and the test
+		// will end with an error "Test did not run". The "launcher" thread will be
+		// killed wherever its execution happens to be, which is a race condition that
+		// means that there may be various exceptions printed or not. However even if it
+		// successfully got past testAppDescriptor.launch(), the test runner which wants
+		// to run in the main thread will never actually run, so the test cannot
+		// mistakenly succeed.
+		return null;
+	}
+
+	@Override
+	public void stop() {
+	}
+
+}