/*******************************************************************************
 * Copyright (c) 2007, 2016 Ericsson 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:
 *     Ericsson	AB		  - Initial implementation of Test cases
 *     Alvaro Sanchez-Leon (Ericsson) - Bug 437562 - Split the dsf-gdb tests to a plug-in and fragment pair
 *     Simon Marchi (Ericsson) - Make canRestart and restart throw Exception instead of Throwable.
 *     Simon Marchi (Ericsson) - Add getThreadData.
 *     Alvaro Sanchez-Leon (Ericsson AB) - [Memory] Make tests run with different values of addressable size (Bug 460241)
 *     Jonah Graham (Kichwa Coders) - Add support for gdb's "set substitute-path" (Bug 472765)
 *******************************************************************************/
package org.eclipse.cdt.tests.dsf.gdb.framework;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.cdt.core.IAddress;
import org.eclipse.cdt.dsf.concurrent.CountingRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.concurrent.ThreadSafeAndProhibitedFromDsfExecutor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IBreakpoints.IBreakpointsTargetDMContext;
import org.eclipse.cdt.dsf.debug.service.IExpressions;
import org.eclipse.cdt.dsf.debug.service.IExpressions.IExpressionDMContext;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.FormattedValueDMContext;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.FormattedValueDMData;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.IFormattedDataDMContext;
import org.eclipse.cdt.dsf.debug.service.IMemory;
import org.eclipse.cdt.dsf.debug.service.IMemory.IMemoryDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMData;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StepType;
import org.eclipse.cdt.dsf.debug.service.ISourceLookup;
import org.eclipse.cdt.dsf.debug.service.ISourceLookup.ISourceLookupDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMData;
import org.eclipse.cdt.dsf.debug.service.IStack.IVariableDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack.IVariableDMData;
import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch;
import org.eclipse.cdt.dsf.gdb.service.IGDBMemory2;
import org.eclipse.cdt.dsf.gdb.service.IGDBProcesses;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIRunControl;
import org.eclipse.cdt.dsf.mi.service.MIStack;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.events.MIRunningEvent;
import org.eclipse.cdt.dsf.mi.service.command.events.MISignalEvent;
import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakInsertInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakListInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakpoint;
import org.eclipse.cdt.dsf.mi.service.command.output.MIDataListRegisterNamesInfo;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.tests.dsf.gdb.framework.SyncUtil.DefaultTimeouts.ETimeout;
import org.eclipse.cdt.tests.dsf.gdb.launching.TestsPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.model.MemoryByte;

/**
 * Timeout wait values are in milliseconds, or WAIT_FOREVER.
 */
public class SyncUtil {

	private static IGDBControl fGdbControl;
	private static IMIRunControl fRunControl;
	private static MIStack fStack;
	private static IExpressions fExpressions;
	private static DsfSession fSession;
	private static IMemory fMemory;

	private static CommandFactory fCommandFactory;
	private static IGDBProcesses fProcessesService;

	private static ISourceLookup fSourceLookup;

	// Static list of register names as obtained directly from GDB.
	// We make it static, key'ed on each version of gdb, so it does not
	// get re-set for every test.
	// Each version of GDB can expose the set of register differently
	private static Map<String, List<String>> fRegisterNames = new HashMap<>();

	// Initialize some common things, once the session has been established
	public static void initialize(DsfSession session) throws Exception {
		fSession = session;

		Runnable runnable = () -> {
			DsfServicesTracker tracker = new DsfServicesTracker(TestsPlugin.getBundleContext(), fSession.getId());

			fGdbControl = tracker.getService(IGDBControl.class);
			fRunControl = tracker.getService(IMIRunControl.class);
			fStack = tracker.getService(MIStack.class);
			fExpressions = tracker.getService(IExpressions.class);
			fProcessesService = tracker.getService(IGDBProcesses.class);
			fMemory = tracker.getService(IMemory.class);
			fCommandFactory = fGdbControl.getCommandFactory();
			fSourceLookup = tracker.getService(ISourceLookup.class);

			tracker.dispose();
		};
		fSession.getExecutor().submit(runnable).get();
	}

	public static MIStoppedEvent step(int numSteps, StepType stepType) throws Throwable {
		return step(numSteps, stepType, false);
	}

	public static MIStoppedEvent step(int numSteps, StepType stepType, boolean reverse) throws Throwable {
		MIStoppedEvent retVal = null;
		for (int i = 0; i < numSteps; i++) {
			retVal = step(stepType, reverse, DefaultTimeouts.get(ETimeout.step));
		}
		return retVal;
	}

	public static MIStoppedEvent step(StepType stepType) throws Throwable {
		return step(stepType, false, DefaultTimeouts.get(ETimeout.step));
	}

	public static MIStoppedEvent step(StepType stepType, boolean reverse, int massagedTimeout) throws Throwable {
		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		return step(containerDmc, stepType, reverse, massagedTimeout);
	}

	public static MIStoppedEvent step(IExecutionDMContext dmc, StepType stepType) throws Throwable {
		return step(dmc, stepType, DefaultTimeouts.get(ETimeout.step));
	}

	public static MIStoppedEvent step(final IExecutionDMContext dmc, final StepType stepType, int massagedTimeout)
			throws Throwable {
		return step(dmc, stepType, false, massagedTimeout);
	}

	public static MIStoppedEvent step(final IExecutionDMContext dmc, final StepType stepType, boolean reverse,
			int massagedTimeout) throws Throwable {
		final ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<>(fSession, MIStoppedEvent.class);

		if (!reverse) {
			fRunControl.getExecutor().submit(() -> {
				// No need for a RequestMonitor since we will wait for the
				// ServiceEvent telling us the program has been suspended again
				switch (stepType) {
				case STEP_INTO:
					fGdbControl.queueCommand(fCommandFactory.createMIExecStep(dmc), null);
					break;
				case STEP_OVER:
					fGdbControl.queueCommand(fCommandFactory.createMIExecNext(dmc), null);
					break;
				case STEP_RETURN:
					fGdbControl.queueCommand(fCommandFactory.createMIExecFinish(fStack.createFrameDMContext(dmc, 0)),
							null);
					break;
				default:
					fail("Unsupported step type; " + stepType.toString());
				}
			});
		} else {
			fRunControl.getExecutor().submit(() -> {
				// No need for a RequestMonitor since we will wait for the
				// ServiceEvent telling us the program has been suspended again
				switch (stepType) {
				case STEP_INTO:
					fGdbControl.queueCommand(fCommandFactory.createMIExecReverseStep(dmc), null);
					break;
				case STEP_OVER:
					fGdbControl.queueCommand(fCommandFactory.createMIExecReverseNext(dmc), null);
					break;
				case STEP_RETURN:
					fGdbControl.queueCommand(fCommandFactory.createMIExecUncall(fStack.createFrameDMContext(dmc, 0)),
							null);
					break;
				default:
					fail("Unsupported step type; " + stepType.toString());
				}
			});
		}

		// Wait for the execution to suspend after the step
		return eventWaitor.waitForEvent(massagedTimeout);
	}

	public static String addBreakpoint(String location) throws Throwable {
		return addBreakpoint(location, DefaultTimeouts.get(ETimeout.addBreakpoint));
	}

	public static String addBreakpoint(String location, int massagedTimeout) throws Throwable {
		return addBreakpoint(location, true, massagedTimeout);
	}

	public static String addBreakpoint(String location, boolean temporary) throws Throwable {
		return addBreakpoint(location, temporary, DefaultTimeouts.get(ETimeout.addBreakpoint));
	}

	private static String addBreakpoint(final String location, final boolean temporary, int massagedTimeout)
			throws Throwable {

		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		final IBreakpointsTargetDMContext bpTargetDmc = DMContexts.getAncestorOfType(containerDmc,
				IBreakpointsTargetDMContext.class);

		Query<MIBreakInsertInfo> query = new Query<MIBreakInsertInfo>() {
			@Override
			protected void execute(DataRequestMonitor<MIBreakInsertInfo> rm) {
				fGdbControl.queueCommand(
						fCommandFactory.createMIBreakInsert(bpTargetDmc, temporary, false, null, 0, location, "0"), rm);
			}
		};

		fGdbControl.getExecutor().execute(query);
		MIBreakInsertInfo info = query.get(massagedTimeout, TimeUnit.MILLISECONDS);
		return info.getMIBreakpoints()[0].getNumber();
	}

	public static String[] getBreakpointList(int timeout) throws Throwable {
		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		final IBreakpointsTargetDMContext bpTargetDmc = DMContexts.getAncestorOfType(containerDmc,
				IBreakpointsTargetDMContext.class);

		Query<MIBreakListInfo> query = new Query<MIBreakListInfo>() {
			@Override
			protected void execute(DataRequestMonitor<MIBreakListInfo> rm) {
				fGdbControl.queueCommand(fCommandFactory.createMIBreakList(bpTargetDmc), rm);
			}
		};

		fGdbControl.getExecutor().execute(query);
		MIBreakListInfo info = query.get(TestsPlugin.massageTimeout(timeout), TimeUnit.MILLISECONDS);
		MIBreakpoint[] breakpoints = info.getMIBreakpoints();

		String[] result = new String[breakpoints.length];
		for (int i = 0; i < breakpoints.length; i++) {
			result[i] = breakpoints[i].getNumber();
		}
		return result;
	}

	private static MIStoppedEvent resumeUntilStopped(final IExecutionDMContext dmc, int massagedTimeout)
			throws Throwable {
		final ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<>(fSession, MIStoppedEvent.class);

		// No need for a RequestMonitor since we will wait for the ServiceEvent telling us the program has been suspended again
		fRunControl.getExecutor()
				.submit(() -> fGdbControl.queueCommand(fCommandFactory.createMIExecContinue(dmc), null));

		// Wait for the execution to suspend after the step
		return eventWaitor.waitForEvent(massagedTimeout);
	}

	public static MIStoppedEvent resumeUntilStopped() throws Throwable {
		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		// Don't call resumeUtilStopped(int timeout) as this will duplicate the timeout massage
		return resumeUntilStopped(containerDmc, DefaultTimeouts.get(ETimeout.resumeUntilStopped));
	}

	public static MIStoppedEvent resumeUntilStopped(int timeout) throws Throwable {
		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		return resumeUntilStopped(containerDmc, TestsPlugin.massageTimeout(timeout));
	}

	public static MIRunningEvent resume(final IExecutionDMContext dmc, int massagedTimeout) throws Throwable {
		final ServiceEventWaitor<MIRunningEvent> eventWaitor = new ServiceEventWaitor<>(fSession, MIRunningEvent.class);

		// No need for a RequestMonitor since we will wait for the ServiceEvent telling us the program has been suspended again
		fRunControl.getExecutor()
				.submit(() -> fGdbControl.queueCommand(fCommandFactory.createMIExecContinue(dmc), null));

		// Wait for the execution to start after the step
		return eventWaitor.waitForEvent(massagedTimeout);
	}

	public static boolean canResume(final IExecutionDMContext execDmc) throws Throwable {
		Query<Boolean> query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				fRunControl.canResume(execDmc, new ImmediateDataRequestMonitor<Boolean>(rm) {
					@Override
					protected void handleSuccess() {
						rm.done(getData());
					}
				});
			}
		};

		fRunControl.getExecutor().execute(query);
		boolean canResume = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
		return canResume;
	}

	public static MIRunningEvent resume() throws Throwable {
		return resume(DefaultTimeouts.get(ETimeout.resume));
	}

	public static MIRunningEvent resume(int massagedTimeout) throws Throwable {
		IContainerDMContext containerDmc = SyncUtil.getContainerContext();
		return resume(containerDmc, massagedTimeout);
	}

	public static void resumeAll() throws Throwable {
		resumeAll(DefaultTimeouts.get(ETimeout.resume));
	}

	public static void resumeAll(int massagedTimeout) throws Throwable {
		IMIExecutionDMContext[] threadDmcs = SyncUtil.getExecutionContexts();
		for (IMIExecutionDMContext thread : threadDmcs) {
			if (canResume(thread)) {
				resume(thread, massagedTimeout);
			}
		}
	}

	public static MIStoppedEvent waitForStop() throws Throwable {
		// Use a direct value to avoid double call to TestsPlugin.massageTimeout
		return waitForStop(10000);
	}

	// This method is risky.  If the command to resume/step execution
	// is sent and the stopped event is received before we call this method
	// here, then we will miss the stopped event.
	// Normally, one should initialize the ServiveEventWaitor before
	// triggering the resume to make sure not to miss the stopped event.
	// However, in some case this method will still work, for instance
	// if there is a sleep in the code between the resume and the time
	// it stops; this will give us plenty of time to call this method.
	public static MIStoppedEvent waitForStop(int timeout) throws Throwable {
		final ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<>(fSession, MIStoppedEvent.class);

		// Wait for the execution to suspend
		return eventWaitor.waitForEvent(TestsPlugin.massageTimeout(timeout));
	}

	public static MIStoppedEvent runToLocation(String location) throws Throwable {
		return runToLocation(location, DefaultTimeouts.get(ETimeout.runToLocation));
	}

	public static MIStoppedEvent runToLocation(String location, int timeout) throws Throwable {
		// Set a temporary breakpoint and run to it.
		// Note that if there were other breakpoints set ahead of this one,
		// they will stop execution earlier than planned
		addBreakpoint(location, true, timeout);
		// Don't provide a timeout so we use the resume default timeout for this step
		// if a timeout value is provided via DefaultTimeouts the value will be massaged twice
		return resumeUntilStopped();
	}

	public static IFrameDMContext getStackFrame(final IExecutionDMContext execCtx, final int level) throws Exception {
		Query<IFrameDMContext> query = new Query<IFrameDMContext>() {
			@Override
			protected void execute(final DataRequestMonitor<IFrameDMContext> rm) {
				fStack.getFrames(execCtx, level, level, new ImmediateDataRequestMonitor<IFrameDMContext[]>(rm) {
					@Override
					protected void handleSuccess() {
						IFrameDMContext[] frameDmcs = getData();
						assert frameDmcs != null;
						assert frameDmcs.length == 1;
						rm.setData(frameDmcs[0]);
						rm.done();
					}
				});
			}
		};

		fSession.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
	}

	/**
	 * Utility method to return a specific frame DM context.
	 */
	@ThreadSafeAndProhibitedFromDsfExecutor("fSession.getExecutor()")
	public static IFrameDMContext getStackFrame(int threadIndex, final int level) throws Exception {
		return getStackFrame(getExecutionContext(threadIndex), level);
	}

	public static Integer getStackDepth(final IExecutionDMContext execCtx) throws Throwable {
		return getStackDepth(execCtx, 0);
	}

	public static Integer getStackDepth(final IExecutionDMContext execCtx, final int maxDepth) throws Throwable {
		Query<Integer> query = new Query<Integer>() {
			@Override
			protected void execute(final DataRequestMonitor<Integer> rm) {
				fStack.getStackDepth(execCtx, maxDepth, rm);
			}
		};

		fSession.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
	}

	public static IFrameDMData getFrameData(final IExecutionDMContext execCtx, final int level) throws Throwable {
		Query<IFrameDMData> query = new Query<IFrameDMData>() {
			@Override
			protected void execute(final DataRequestMonitor<IFrameDMData> rm) {
				fStack.getFrames(execCtx, level, level, new ImmediateDataRequestMonitor<IFrameDMContext[]>(rm) {
					@Override
					protected void handleSuccess() {
						IFrameDMContext[] frameDmcs = getData();
						assert frameDmcs != null;
						assert frameDmcs.length == 1;
						fStack.getFrameData(frameDmcs[0], rm);
					}
				});
			}
		};

		fSession.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
	}

	public static IFrameDMData getFrameData(final int threadId, final int level) throws Throwable {
		return getFrameData(getExecutionContext(threadId), level);
	}

	public static IThreadDMData getThreadData(final int threadId)
			throws InterruptedException, ExecutionException, TimeoutException {
		final IProcessDMContext processContext = DMContexts.getAncestorOfType(SyncUtil.getContainerContext(),
				IProcessDMContext.class);

		Query<IThreadDMData> query = new Query<IThreadDMData>() {
			@Override
			protected void execute(DataRequestMonitor<IThreadDMData> rm) {
				IThreadDMContext threadDmc = fProcessesService.createThreadContext(processContext,
						Integer.toString(threadId));
				fProcessesService.getExecutionData(threadDmc, rm);

			}
		};

		fSession.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
	}

	public static IExpressionDMContext createExpression(final IDMContext parentCtx, final String expression)
			throws Throwable {
		Callable<IExpressionDMContext> callable = () -> fExpressions.createExpression(parentCtx, expression);
		return fSession.getExecutor().submit(callable).get();
	}

	public static IExpressionDMContext[] getSubExpressions(final IExpressionDMContext dmc)
			throws InterruptedException, ExecutionException {
		Query<IExpressionDMContext[]> query = new Query<IExpressionDMContext[]>() {
			@Override
			protected void execute(DataRequestMonitor<IExpressionDMContext[]> rm) {
				fExpressions.getSubExpressions(dmc, rm);
			}
		};

		fSession.getExecutor().execute(query);
		return query.get();
	}

	/*
	 * Like getSubExpressions, but for cases where we know there will be only
	 * one child.
	 */
	public static IExpressionDMContext getSubExpression(final IExpressionDMContext dmc)
			throws InterruptedException, ExecutionException {
		IExpressionDMContext[] subExpressions = SyncUtil.getSubExpressions(dmc);

		assertEquals(1, subExpressions.length);

		return subExpressions[0];
	}

	public static String getExpressionValue(final IExpressionDMContext exprDmc, final String format) throws Throwable {
		Query<String> query = new Query<String>() {
			@Override
			protected void execute(final DataRequestMonitor<String> rm) {
				FormattedValueDMContext valueDmc = fExpressions.getFormattedValueContext(exprDmc, format);
				fExpressions.getFormattedExpressionValue(valueDmc,
						new ImmediateDataRequestMonitor<FormattedValueDMData>(rm) {
							@Override
							protected void handleSuccess() {
								rm.done(getData().getFormattedValue());
							}
						});
			}
		};

		fSession.getExecutor().execute(query);
		return query.get();
	}

	public static FormattedValueDMContext getFormattedValue(final IFormattedValues service,
			final IFormattedDataDMContext dmc, final String formatId) throws Throwable {
		Callable<FormattedValueDMContext> callable = () -> service.getFormattedValueContext(dmc, formatId);
		return fSession.getExecutor().submit(callable).get();
	}

	public static IMIExecutionDMContext createExecutionContext(final IContainerDMContext parentCtx, final int threadId)
			throws Throwable {
		Callable<IMIExecutionDMContext> callable = () -> {
			String threadIdStr = Integer.toString(threadId);
			IProcessDMContext processDmc = DMContexts.getAncestorOfType(parentCtx, IProcessDMContext.class);
			IThreadDMContext threadDmc = fProcessesService.createThreadContext(processDmc, threadIdStr);
			return fProcessesService.createExecutionContext(parentCtx, threadDmc, threadIdStr);
		};
		return fSession.getExecutor().submit(callable).get();
	}

	static class DefaultTimeouts {

		/**
		 * Overridable default timeout values. An override is specified using a
		 * system property that is "dsf.gdb.tests.timeout.default." plus the
		 * name of the enum below.
		 */
		enum ETimeout {
			addBreakpoint, deleteBreakpoint, getBreakpointList, createExecutionContext, createExpression,
			getFormattedValue, getStackFrame, resume, resumeUntilStopped, runToLine, runToLocation, step, waitForStop
		}

		/**
		 * Map of timeout enums to their <b>harcoded</b> default value )in
		 * milliseconds). These can be individually overridden with a system
		 * property.
		 *
		 * <p>
		 * In practice, these operations are either very quick or the amount of
		 * time is hard to predict (depends on what the test is doing). For ones
		 * that are quick, we allot 1 second, which is ample. For the unknowns
		 * we allows 10 seconds, which is probably ample in most cases. Tests
		 * can provide larger values as needed in specific SyncUtil calls.
		 */
		private static Map<ETimeout, Integer> sTimeouts = new HashMap<>();
		static {
			sTimeouts.put(ETimeout.addBreakpoint, 1000);
			sTimeouts.put(ETimeout.deleteBreakpoint, 1000);
			sTimeouts.put(ETimeout.getBreakpointList, 1000);
			sTimeouts.put(ETimeout.createExecutionContext, 1000);
			sTimeouts.put(ETimeout.createExpression, 1000);
			sTimeouts.put(ETimeout.getFormattedValue, 1000);
			sTimeouts.put(ETimeout.getStackFrame, 1000);
			sTimeouts.put(ETimeout.resume, 1000);
			sTimeouts.put(ETimeout.resumeUntilStopped, 10000); // 10 seconds
			sTimeouts.put(ETimeout.runToLine, 10000); // 10 seconds
			sTimeouts.put(ETimeout.runToLocation, 10000); // 10 seconds
			sTimeouts.put(ETimeout.step, 1000);
			sTimeouts.put(ETimeout.waitForStop, 10000); // 10 seconds
		}

		/**
		 * Get the default timeout to use when the caller of a SyncUtil method
		 * doesn't specify one. We honor overrides specified via system
		 * properties, as well as apply the multiplier that can also be
		 * specified via a system property.
		 *
		 * @param timeout
		 *            the timeout enum
		 * @return the default value
		 */
		static int get(ETimeout timeout) {
			int value = -1;
			final String propname = "dsf.gdb.tests.timeout.default." + timeout.toString();
			final String prop = System.getProperty(propname);
			if (prop != null) {
				try {
					value = Integer.valueOf(value);
					if (value < 0) {
						TestsPlugin.log(new Status(IStatus.ERROR, TestsPlugin.getUniqueIdentifier(), "\"" + propname //$NON-NLS-1$
								+ "\" property incorrectly specified. Should be an integer value or not specified at all."));
						value = -1;
					}
				} catch (NumberFormatException exc) {
					TestsPlugin.log(new Status(IStatus.ERROR, TestsPlugin.getUniqueIdentifier(), "\"" + propname //$NON-NLS-1$
							+ "\" property incorrectly specified. Should be an integer value or not specified at all."));
					value = -1;
				}
			}

			if (value == -1) {
				value = sTimeouts.get(timeout);
			}
			assert value >= 0;
			return TestsPlugin.massageTimeout(value);
		}
	}

	/**
	 * Utility method to return the container DM context. This can be used only by
	 * tests that deal with a single heavyweight process. If more than one
	 * process is available, this method will fail.
	 *
	 * <p>
	 * This must NOT be called from the DSF executor.
	 *
	 * @return the process context
	 * @throws InterruptedException
	 * @throws TimeoutException
	 * @throws ExecutionException
	 */
	@ThreadSafeAndProhibitedFromDsfExecutor("fSession.getExecutor()")
	public static IContainerDMContext getContainerContext()
			throws InterruptedException, ExecutionException, TimeoutException {
		assert !fProcessesService.getExecutor().isInExecutorThread();

		Query<IContainerDMContext> query = new Query<IContainerDMContext>() {
			@Override
			protected void execute(final DataRequestMonitor<IContainerDMContext> rm) {
				fProcessesService.getProcessesBeingDebugged(fGdbControl.getContext(),
						new ImmediateDataRequestMonitor<IDMContext[]>() {
							@Override
							protected void handleCompleted() {
								if (isSuccess()) {
									IDMContext[] contexts = getData();
									assertNotNull("invalid return value from service", contexts);
									assertEquals("unexpected number of processes", 1, contexts.length);
									IDMContext context = contexts[0];
									assertNotNull("unexpected process context type ", context);
									rm.done((IContainerDMContext) context);
								} else {
									rm.done(getStatus());
								}
							}
						});
			}
		};

		fGdbControl.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(2000), TimeUnit.MILLISECONDS);
	}

	/**
	 * Utility method to return all thread execution contexts.
	 * @throws TimeoutException
	 * @throws ExecutionException
	 */
	@ThreadSafeAndProhibitedFromDsfExecutor("fSession.getExecutor()")
	public static IMIExecutionDMContext[] getExecutionContexts()
			throws InterruptedException, ExecutionException, TimeoutException {
		assert !fProcessesService.getExecutor().isInExecutorThread();

		final IContainerDMContext containerDmc = SyncUtil.getContainerContext();

		Query<IMIExecutionDMContext[]> query = new Query<IMIExecutionDMContext[]>() {
			@Override
			protected void execute(final DataRequestMonitor<IMIExecutionDMContext[]> rm) {
				fProcessesService.getProcessesBeingDebugged(containerDmc,
						new ImmediateDataRequestMonitor<IDMContext[]>() {
							@Override
							protected void handleCompleted() {
								if (isSuccess()) {
									IDMContext[] threads = getData();
									assertNotNull("invalid return value from service", threads);
									rm.setData((IMIExecutionDMContext[]) threads);
								} else {
									rm.setStatus(getStatus());
								}
								rm.done();
							}
						});
			}
		};

		fGdbControl.getExecutor().execute(query);
		return query.get(TestsPlugin.massageTimeout(2000), TimeUnit.MILLISECONDS);
	}

	/**
	 * Utility method to return a specific execution DM context.
	 * @throws TimeoutException
	 * @throws ExecutionException
	 */
	@ThreadSafeAndProhibitedFromDsfExecutor("fSession.getExecutor()")
	public static IMIExecutionDMContext getExecutionContext(int threadIndex)
			throws InterruptedException, ExecutionException, TimeoutException {
		IMIExecutionDMContext[] threads = getExecutionContexts();
		assertTrue("unexpected number of threads", threadIndex < threads.length);
		assertNotNull("unexpected thread context type ", threads[threadIndex]);
		return threads[threadIndex];
	}

	/**
	 * Check if the restart operation is supported
	 */
	public static boolean canRestart() throws Exception {
		final IContainerDMContext containerDmc = getContainerContext();

		// Check if restart is allowed
		Query<Boolean> query = new Query<Boolean>() {
			@Override
			protected void execute(final DataRequestMonitor<Boolean> rm) {
				fProcessesService.canRestart(containerDmc, new ImmediateDataRequestMonitor<Boolean>(rm) {
					@Override
					protected void handleSuccess() {
						rm.setData(getData());
						rm.done();
					}
				});

			}
		};

		fGdbControl.getExecutor().execute(query);
		boolean canRestart = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
		return canRestart;
	}

	/**
	 * Restart the program.
	 */
	public static MIStoppedEvent restart(final GdbLaunch launch) throws Exception {
		final IContainerDMContext containerDmc = getContainerContext();

		// If we are calling this method, the restart operation should be allowed
		if (!canRestart()) {
			throw new CoreException(new Status(IStatus.ERROR, TestsPlugin.PLUGIN_ID, "Unable to restart"));
		}

		// Now wait for the stopped event of the restart
		final ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<>(fSession, MIStoppedEvent.class);

		// Perform the restart
		Query<IContainerDMContext> query2 = new Query<IContainerDMContext>() {
			@Override
			protected void execute(final DataRequestMonitor<IContainerDMContext> rm) {
				Map<String, Object> attributes = null;
				try {
					attributes = launch.getLaunchConfiguration().getAttributes();
				} catch (CoreException e) {
				}

				fProcessesService.restart(containerDmc, attributes, rm);
			}
		};

		fGdbControl.getExecutor().execute(query2);
		query2.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);

		MIStoppedEvent event = eventWaitor.waitForEvent(DefaultTimeouts.get(ETimeout.waitForStop));
		if (event instanceof MISignalEvent) {
			// This is not the stopped event we were waiting for.  Get the next one.
			event = eventWaitor.waitForEvent(DefaultTimeouts.get(ETimeout.waitForStop));
		}
		return event;
	}

	public static IVariableDMData[] getLocals(final IFrameDMContext frameDmc) throws Throwable {
		Query<IVariableDMData[]> query = new Query<IVariableDMData[]>() {
			@Override
			protected void execute(final DataRequestMonitor<IVariableDMData[]> rm) {
				fStack.getLocals(frameDmc, new ImmediateDataRequestMonitor<IVariableDMContext[]>() {
					@Override
					protected void handleCompleted() {
						if (isSuccess()) {
							IVariableDMContext[] varDmcs = getData();
							final List<IVariableDMData> localsDMData = new ArrayList<>();
							final CountingRequestMonitor crm = new CountingRequestMonitor(
									ImmediateExecutor.getInstance(), rm) {
								@Override
								protected void handleSuccess() {
									rm.done(localsDMData.toArray(new IVariableDMData[localsDMData.size()]));
								}
							};

							for (IVariableDMContext varDmc : varDmcs) {
								fStack.getVariableData(varDmc, new ImmediateDataRequestMonitor<IVariableDMData>(crm) {
									@Override
									public void handleSuccess() {
										localsDMData.add(getData());
										crm.done();
									}
								});
							}
							crm.setDoneCount(varDmcs.length);
						} else {
							rm.done();
						}
					}
				});
			}
		};

		fSession.getExecutor().execute(query);
		IVariableDMData[] result = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
		return result;
	}

	/**
	 * Get the registers directly from GDB (without using the registers service)
	 * @param gdbVersion
	 * @param context
	 * @return
	 * @throws Throwable
	 */
	public static List<String> getRegistersFromGdb(String gdbVersion, IDMContext context) throws Throwable {
		if (!fRegisterNames.containsKey(gdbVersion)) {
			// The tests must run on different machines, so the set of registers can change.
			// To deal with this we ask GDB for the list of registers.
			// Note that we send an MI Command in this code and do not use the IRegister service;
			// this is because we want to test the service later, comparing it to what we find
			// by asking GDB directly.
			Query<MIDataListRegisterNamesInfo> query = new Query<MIDataListRegisterNamesInfo>() {
				@Override
				protected void execute(DataRequestMonitor<MIDataListRegisterNamesInfo> rm) {
					IContainerDMContext containerDmc = DMContexts.getAncestorOfType(context, IContainerDMContext.class);
					fGdbControl.queueCommand(
							fGdbControl.getCommandFactory().createMIDataListRegisterNames(containerDmc), rm);
				}
			};
			fSession.getExecutor().execute(query);

			MIDataListRegisterNamesInfo data = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
			String[] names = data.getRegisterNames();

			// Remove registers with empty names since the service also
			// remove them. I don't know why GDB returns such empty names.
			List<String> registerNames = new LinkedList<>();
			for (String name : names) {
				if (!name.isEmpty()) {
					registerNames.add(name);
				}
			}
			assertNotEquals(
					"Test does not make sense, and has probably completely failed, as there are no register names",
					Collections.emptyList(), registerNames);
			fRegisterNames.put(gdbVersion, registerNames);
		}
		return fRegisterNames.get(gdbVersion);
	}

	/**
	 * Read data from memory.
	 *
	 * @param dmc		the data model context
	 * @param address	the memory block address
	 * @param offset	the offset in the buffer
	 * @param wordSize	the size of a word, in octets
	 * @param count		the number of bytes to read
	 * @return			the memory content
	 * @throws InterruptedException
	 * @throws ExecutionException
	 */
	public static MemoryByte[] readMemory(final IMemoryDMContext dmc, final IAddress address, final long offset,
			final int wordSize, final int count) throws InterruptedException, ExecutionException {
		Query<MemoryByte[]> query = new Query<MemoryByte[]>() {
			@Override
			protected void execute(DataRequestMonitor<MemoryByte[]> rm) {
				fMemory.getMemory(dmc, address, offset, wordSize, count, rm);
			}
		};

		fMemory.getExecutor().execute(query);

		return query.get();
	}

	/**
	 * Write data to memory.
	 *
	 * @param dmc		the data model context
	 * @param address	the memory block address (could be an expression)
	 * @param offset	the offset from address
	 * @param wordSize	the word size, in octets
	 * @param count		the number of bytes to write
	 * @param buffer	the byte buffer to write from
	 * @throws InterruptedException
	 * @throws ExecutionException
	 */
	public static void writeMemory(final IMemoryDMContext dmc, final IAddress address, final long offset,
			final int wordSize, final int count, final byte[] buffer) throws InterruptedException, ExecutionException {
		Query<Void> query = new Query<Void>() {
			@Override
			protected void execute(DataRequestMonitor<Void> rm) {
				fMemory.setMemory(dmc, address, offset, wordSize, count, buffer, rm);
			}
		};

		fMemory.getExecutor().execute(query);

		query.get();
	}

	/**
	 * Fill memory with a pattern.
	 *
	 * @param dmc		the data model context
	 * @param address	the memory block address (could be an expression)
	 * @param offset	the offset from address
	 * @param wordSize	the word size, in octets
	 * @param count		the number of times the pattern is to be written
	 * @param pattern	the pattern to write
	 * @throws InterruptedException
	 * @throws ExecutionException
	 */
	public static void fillMemory(final IMemoryDMContext dmc, final IAddress address, final long offset,
			final int wordSize, final int count, final byte[] pattern) throws InterruptedException, ExecutionException {
		Query<Void> query = new Query<Void>() {
			@Override
			protected void execute(DataRequestMonitor<Void> rm) {
				fMemory.fillMemory(dmc, address, offset, wordSize, count, pattern, rm);
			}
		};

		fMemory.getExecutor().execute(query);

		query.get();
	}

	/**
	 * Get the addressable size of a memory context, in octets. The addressable
	 * size is the number of octets in each memory "cell".
	 *
	 * @param dmc
	 *            the memory data model context
	 * @return the addressable size, in octets.
	 */
	public static int readAddressableSize(final IMemoryDMContext dmc) {
		assert (fMemory instanceof IGDBMemory2);
		final IGDBMemory2 memoryService = (IGDBMemory2) fMemory;

		return memoryService.getAddressableSize(dmc);
	}

	/**
	 * Get the byte order of a memory context.
	 *
	 * @param dmc
	 *            the memory data model context
	 * @return the byte order
	 */
	public static ByteOrder getMemoryByteOrder(final IMemoryDMContext dmc) {
		assert (fMemory instanceof IGDBMemory2);
		final IGDBMemory2 memoryService = (IGDBMemory2) fMemory;

		return memoryService.isBigEndian(dmc) ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
	}

	/**
	 * Get the source using the {@link ISourceLookup} service.
	 *
	 * Wrapper around
	 * {@link ISourceLookup#getSource(ISourceLookupDMContext, String, DataRequestMonitor)}
	 */
	public static Object getSource(final String debuggerPath) throws Exception {
		Query<Object> query = new Query<Object>() {
			@Override
			protected void execute(DataRequestMonitor<Object> rm) {
				final ISourceLookupDMContext ctx = DMContexts.getAncestorOfType(fGdbControl.getContext(),
						ISourceLookupDMContext.class);
				fSourceLookup.getSource(ctx, debuggerPath, rm);
			}
		};

		fSourceLookup.getExecutor().execute(query);

		return query.get();
	}
}
