/*******************************************************************************
 * Copyright (c) 2003, 2015 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 - Initial API and implementation
 *******************************************************************************/
package org.eclipse.core.tests.runtime.jobs;

import junit.framework.TestSuite;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.core.tests.harness.FussyProgressMonitor;
import org.eclipse.core.tests.harness.TestBarrier;

/**
 * Tests API methods IJobManager.beginRule and IJobManager.endRule
 */
public class BeginEndRuleTest extends AbstractJobManagerTest {

	/**
	 * This runnable will try to end the given rule in the Job Manager
	 */
	private class RuleEnder implements Runnable {
		private ISchedulingRule rule;
		private int[] status;

		public RuleEnder(ISchedulingRule rule, int[] status) {
			this.rule = rule;
			this.status = status;
		}

		@Override
		public void run() {
			try {
				status[0] = TestBarrier.STATUS_RUNNING;
				manager.endRule(rule);
				fail("Ending Rule");

			} catch (RuntimeException e) {
				//should fail
			}

		}
	}

	public static TestSuite suite() {
		return new TestSuite(BeginEndRuleTest.class);
		//		TestSuite suite = new TestSuite();
		//		suite.addTest(new BeginEndRuleTest("testComplexRuleStarting"));
		//		return suite;
	}

	public BeginEndRuleTest() {
		super();
	}

	public BeginEndRuleTest(String name) {
		super(name);
	}

	public void testComplexRuleStarting() {
		//test how the manager reacts when several different threads try to begin conflicting rules
		final int NUM_THREADS = 3;
		//array to communicate with the launched threads
		final int[] status = {TestBarrier.STATUS_WAIT_FOR_START, TestBarrier.STATUS_WAIT_FOR_START, TestBarrier.STATUS_WAIT_FOR_START};
		//number of times to start each rule
		int NUM_REPEATS = 10;

		Job[] jobs = new Job[NUM_THREADS];
		jobs[0] = new JobRuleRunner("ComplexJob1", new PathRule("/testComplexRuleStarting"), status, 0, NUM_REPEATS, true);
		jobs[1] = new JobRuleRunner("ComplexJob2", new PathRule("/testComplexRuleStarting/B"), status, 1, NUM_REPEATS, true);
		jobs[2] = new JobRuleRunner("ComplexJob3", new PathRule("/testComplexRuleStarting/B/C"), status, 2, NUM_REPEATS, true);

		//schedule the jobs
		for (Job job : jobs)
			job.schedule();

		//wait until all the jobs start
		for (int i = 0; i < jobs.length; i++)
			TestBarrier.waitForStatus(status, i, TestBarrier.STATUS_START);

		//all jobs should be running
		//the status flag should be set to START
		for (int i = 0; i < status.length; i++) {
			assertEquals("1." + i, Job.RUNNING, jobs[i].getState());
			assertEquals("2." + i, TestBarrier.STATUS_START, status[i]);
		}

		//the order that the jobs will be executed
		int[] order = {0, 1, 2};

		for (int j = 0; j < NUM_REPEATS; j++) {
			//let the first job in the order run
			status[order[0]] = TestBarrier.STATUS_WAIT_FOR_RUN;
			//wait until the first job in the order reads the flag
			TestBarrier.waitForStatus(status, order[0], TestBarrier.STATUS_RUNNING);

			//let all subsequent jobs run (they will be blocked)
			//before starting next job, wait until previous job is blocked by JobManager
			for (int i = 1; i < order.length; i++) {
				status[order[i]] = TestBarrier.STATUS_WAIT_FOR_RUN;
				TestBarrier.waitForStatus(status, order[i], TestBarrier.STATUS_BLOCKED);
			}

			//the first job should be running, the remaining jobs should be waiting
			assertEquals("3.0", TestBarrier.STATUS_RUNNING, status[order[0]]);
			assertEquals("3.0", TestBarrier.STATUS_BLOCKED, status[order[1]]);
			assertEquals("3.0", TestBarrier.STATUS_BLOCKED, status[order[2]]);

			//let the first job finish
			status[order[0]] = TestBarrier.STATUS_WAIT_FOR_DONE;
			TestBarrier.waitForStatus(status, order[0], TestBarrier.STATUS_DONE);

			//the remaining jobs will now compete for execution (order NOT guaranteed)
			//let them both start and wait until they complete
			int doneCount = 0;
			while (doneCount < 2) {
				if (status[order[1]] == TestBarrier.STATUS_RUNNING) {
					status[order[1]] = TestBarrier.STATUS_WAIT_FOR_DONE;
					TestBarrier.waitForStatus(status, order[1], TestBarrier.STATUS_DONE);
					doneCount++;
				}
				if (status[order[2]] == TestBarrier.STATUS_RUNNING) {
					status[order[2]] = TestBarrier.STATUS_WAIT_FOR_DONE;
					TestBarrier.waitForStatus(status, order[2], TestBarrier.STATUS_DONE);
					doneCount++;
				}
			}
			//change the order of the jobs, nothing should change in the execution
			int temp = order[0];
			order[0] = order[2];
			order[2] = order[1];
			order[1] = temp;
		}

		//wait until all jobs are done
		for (int element : order) {
			waitForEnd(jobs[element]);
		}

		for (int i = 0; i < jobs.length; i++) {
			//check that the final status of all jobs is correct
			assertEquals("9." + i, TestBarrier.STATUS_DONE, status[i]);
			assertEquals("10." + i, Job.NONE, jobs[i].getState());
			assertEquals("11." + i, IStatus.OK, jobs[i].getResult().getSeverity());
		}
	}

	public void testSimpleRuleStarting() {
		//start two jobs, each of which will begin and end a rule several times
		//while one job starts a rule, the second job's call to begin rule should block that thread
		//until the first job calls end rule
		final int[] status = {TestBarrier.STATUS_WAIT_FOR_START, TestBarrier.STATUS_WAIT_FOR_START};
		//number of repetitions of beginning and ending the rule
		final int NUM_REPEATS = 10;
		Job[] jobs = new Job[2];
		jobs[0] = new JobRuleRunner("SimpleJob1", new PathRule("/testSimpleRuleStarting"), status, 0, NUM_REPEATS, false);
		jobs[1] = new JobRuleRunner("SimpleJob2", new PathRule("/testSimpleRuleStarting/B"), status, 1, NUM_REPEATS, false);

		//schedule both jobs to start their execution
		jobs[0].schedule();
		jobs[1].schedule();

		//make sure both jobs are running and their respective run methods have been invoked
		//waitForStart(jobs[1]);
		TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_START);
		TestBarrier.waitForStatus(status, 1, TestBarrier.STATUS_START);

		assertEquals("2.0", Job.RUNNING, jobs[0].getState());
		assertEquals("2.1", Job.RUNNING, jobs[1].getState());
		assertEquals("2.2", TestBarrier.STATUS_START, status[0]);
		assertEquals("2.3", TestBarrier.STATUS_START, status[1]);

		//the order of execution of the jobs (by their index in the status array)
		int first = 0;
		int second = 1;

		//now both jobs are waiting for the STATUS_WAIT_FOR_RUN flag
		for (int j = 0; j < NUM_REPEATS; j++) {
			//let the first job start executing
			status[first] = TestBarrier.STATUS_WAIT_FOR_RUN;

			//wait for the first job to read the flag
			TestBarrier.waitForStatus(status, first, TestBarrier.STATUS_RUNNING);

			//let the second job start, its thread will be blocked by the beginRule method
			status[second] = TestBarrier.STATUS_WAIT_FOR_RUN;

			//only the first job should be running
			//the other job should be blocked by the beginRule method
			assertEquals("3.1", TestBarrier.STATUS_RUNNING, status[first]);
			assertEquals("3.2", TestBarrier.STATUS_WAIT_FOR_RUN, status[second]);

			//let the first job finish execution and call endRule
			//the second thread will then become unblocked
			status[first] = TestBarrier.STATUS_WAIT_FOR_DONE;

			//wait until the first job is done
			TestBarrier.waitForStatus(status, first, TestBarrier.STATUS_DONE);

			//now wait until the second job begins execution
			TestBarrier.waitForStatus(status, second, TestBarrier.STATUS_RUNNING);

			//the first job is done, the second job is executing
			assertEquals("4.1", TestBarrier.STATUS_DONE, status[first]);
			assertEquals("4.2", TestBarrier.STATUS_RUNNING, status[second]);

			//let the second job finish execution
			status[second] = TestBarrier.STATUS_WAIT_FOR_DONE;

			//wait until the second job is finished
			TestBarrier.waitForStatus(status, second, TestBarrier.STATUS_DONE);

			//both jobs are done now
			assertEquals("5.1", TestBarrier.STATUS_DONE, status[first]);
			assertEquals("5.2", TestBarrier.STATUS_DONE, status[second]);

			//flip the order of execution of the jobs
			int temp = first;
			first = second;
			second = temp;
		}

		//wait until both jobs are done
		waitForEnd(jobs[second]);
		waitForEnd(jobs[first]);

		//check that the final status of both jobs is correct
		assertEquals("6.1", TestBarrier.STATUS_DONE, status[0]);
		assertEquals("6.2", TestBarrier.STATUS_DONE, status[1]);
		assertEquals("6.3", Job.NONE, jobs[0].getState());
		assertEquals("6.4", Job.NONE, jobs[1].getState());
		assertEquals("6.5", IStatus.OK, jobs[0].getResult().getSeverity());
		assertEquals("6.6", IStatus.OK, jobs[1].getResult().getSeverity());
	}

	public void testComplexRuleContainment() {
		ISchedulingRule rules[] = new ISchedulingRule[4];

		rules[0] = new PathRule("/testComplexRuleContainment");
		rules[1] = new PathRule("/testComplexRuleContainment/B");
		rules[2] = new PathRule("/testComplexRuleContainment/B/C");
		rules[3] = new PathRule("/testComplexRuleContainment/D");

		//adding multiple rules in correct order
		int RULE_REPEATS = 10;
		try {
			for (int i = 0; i < rules.length - 1; i++) {
				for (int j = 0; j < RULE_REPEATS; j++) {
					manager.beginRule(rules[i], null);
				}
			}
			for (int i = rules.length - 1; i > 0; i--) {
				for (int j = 0; j < RULE_REPEATS; j++) {
					manager.endRule(rules[i - 1]);
				}
			}
		} catch (RuntimeException e) {
			fail("4.0");
		}

		//adding rules in proper order, then adding a rule from a bypassed branch
		//trying to end previous rules should not work
		for (ISchedulingRule rule : rules) {
			manager.beginRule(rule, null);
		}
		try {
			manager.endRule(rules[2]);
			fail("4.1");
		} catch (RuntimeException e) {
			//should fail
			try {
				manager.endRule(rules[1]);
				fail("4.2");
			} catch (RuntimeException e1) {
				//should fail
				try {
					manager.endRule(rules[0]);
					fail("4.3");
				} catch (RuntimeException e2) {
					//should fail
				}
			}
		}
		for (int i = rules.length; i > 0; i--) {
			manager.endRule(rules[i - 1]);
		}
	}

	public void _testEndNullRule() {
		//see bug #43460
		//end null IScheduleRule without begin
		try {
			manager.endRule(null);
			fail("1.1");
		} catch (RuntimeException e) {
			//should fail
		}
	}

	public void testFailureCase() {
		ISchedulingRule rule1 = new IdentityRule();
		ISchedulingRule rule2 = new IdentityRule();

		//end without begin
		try {
			manager.endRule(rule1);
			fail("1.0");
		} catch (RuntimeException e) {
			//should fail
		}
		//simple mismatched begin/end
		manager.beginRule(rule1, null);
		try {
			manager.endRule(rule2);
			fail("1.2");
		} catch (RuntimeException e) {
			//should fail
		}
		//should still be able to end the original rule
		manager.endRule(rule1);

		//mismatched begin/end, ending a null rule
		manager.beginRule(rule1, null);
		try {
			manager.endRule(null);
			fail("1.3");
		} catch (RuntimeException e) {
			//should fail
		}
		//should still be able to end the original rule
		manager.endRule(rule1);
	}

	/**
	 * Tests create a job with one scheduling rule, and then attempting
	 * to acquire an unrelated rule from within that job.
	 */
	public void testFailedNestRuleInJob() {
		final ISchedulingRule rule1 = new PathRule("/testFailedNestRuleInJob/A/");
		final ISchedulingRule rule2 = new PathRule("/testFailedNestRuleInJob/B/");
		final Exception[] exception = new Exception[1];
		Job job = new Job("testFailedNestRuleInJob") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					try {
						manager.beginRule(rule2, monitor);
					} finally {
						manager.endRule(rule2);
					}
				} catch (RuntimeException e) {
					exception[0] = e;
				}
				return Status.OK_STATUS;
			}
		};
		job.setRule(rule1);
		job.schedule();
		waitForEnd(job);
		assertTrue("1.0", exception[0] != null);
		assertTrue("1.1", exception[0].getMessage().indexOf("does not match outer scope rule") > 0);
	}

	public void testNestedCase() {
		ISchedulingRule rule1 = new PathRule("/testNestedCase");
		ISchedulingRule rule2 = new PathRule("/testNestedCase/B");

		//ending an outer rule before an inner one
		manager.beginRule(rule1, null);
		manager.beginRule(rule2, null);
		try {
			manager.endRule(rule1);
			fail("2.0");
		} catch (RuntimeException e) {
			//should fail
		}
		manager.endRule(rule2);
		manager.endRule(rule1);

		//ending a rule that is not open
		manager.beginRule(rule1, null);
		manager.beginRule(rule2, null);
		try {
			manager.endRule(null);
			fail("2.1");
		} catch (RuntimeException e) {
			//should fail
		}
		manager.endRule(rule2);
		manager.endRule(rule1);

		//adding rules starting with null rule
		try {
			manager.beginRule(null, null);
			manager.beginRule(rule1, null);
			manager.endRule(rule1);
			manager.beginRule(rule2, null);
			manager.endRule(rule2);
			manager.endRule(null);
		} catch (RuntimeException e) {
			//should not fail
			fail("2.2");
		}

		//adding numerous instances of the same rule
		int NUM_ADDITIONS = 100;
		try {
			for (int i = 0; i < NUM_ADDITIONS; i++) {
				manager.beginRule(rule1, null);
			}
			for (int i = 0; i < NUM_ADDITIONS; i++) {
				manager.endRule(rule1);
			}
		} catch (RuntimeException e) {
			//should not fail
			fail("2.3");
		}

		//adding numerous instances of the null rule
		try {
			for (int i = 0; i < NUM_ADDITIONS; i++) {
				manager.beginRule(null, null);
			}
			manager.beginRule(rule1, null);
			manager.endRule(rule1);
			for (int i = 0; i < NUM_ADDITIONS; i++) {
				manager.endRule(null);
			}
		} catch (RuntimeException e) {
			//should not fail
			fail("2.4");
		}
	}

	/**
	 * Tests a failure where canceling an attempt to beginRule resulted in implicit jobs
	 * being recycled before they were finished.
	 */
	public void testBug44299() {
		ISchedulingRule rule = new IdentityRule();
		FussyProgressMonitor monitor = new FussyProgressMonitor();
		manager.beginRule(rule, monitor);
		int[] status = new int[1];
		SimpleRuleRunner runner = new SimpleRuleRunner(rule, status, monitor);
		new Thread(runner).start();

		//wait for the job to start
		TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);
		//give the job a chance to enter the wait loop
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			//ignore
		}
		//cancel the monitor
		monitor.setCanceled(true);

		TestBarrier.waitForStatus(status, TestBarrier.STATUS_DONE);
		if (runner.exception != null)
			fail("1.0", runner.exception);

		//finally clear the rule
		manager.endRule(rule);
	}

	public void testRuleContainment() {
		ISchedulingRule rules[] = new ISchedulingRule[4];

		rules[0] = new PathRule("/testRuleContainment");
		rules[1] = new PathRule("/testRuleContainment/B");
		rules[2] = new PathRule("/testRuleContainment/B/C");
		rules[3] = new PathRule("/testRuleContainment/D");

		//simple addition of rules in incorrect containment order
		manager.beginRule(rules[1], null);
		try {
			manager.beginRule(rules[0], null);
			fail("3.0");
		} catch (RuntimeException e) {
			//should fail
		} finally {
			//still need to end the rule
			manager.endRule(rules[0]);
		}
		manager.endRule(rules[1]);

		//adding rules in proper order, then adding a rule from different hierarchy
		manager.beginRule(rules[1], null);
		manager.beginRule(rules[2], null);
		try {
			manager.beginRule(rules[3], null);
			fail("3.2");
		} catch (RuntimeException e) {
			//should fail
		} finally {
			manager.endRule(rules[3]);
		}
		//should still be able to end the rules
		manager.endRule(rules[2]);
		manager.endRule(rules[1]);
	}

	public void testSimpleOtherThreadAccess() {
		//ending a rule started on this thread from another thread
		ISchedulingRule rule1 = new IdentityRule();
		int[] status = {TestBarrier.STATUS_START};
		Thread endingThread = new Thread(new RuleEnder(rule1, status));
		manager.beginRule(rule1, null);

		endingThread.start();
		TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);

		try {
			endingThread.join();
		} catch (InterruptedException e) {
			//ignore
		}
		//the thread should be dead now
		assertTrue("1.0", !endingThread.isAlive());

		//should be able to end the rule from this thread
		manager.endRule(rule1);

		//starting several rules on this thread, and trying to end them from other threads
		ISchedulingRule rules[] = new ISchedulingRule[3];

		rules[0] = new PathRule("/testSimpleOtherThreadAccess");
		rules[1] = new PathRule("/testSimpleOtherThreadAccess/B");
		rules[2] = new PathRule("/testSimpleOtherThreadAccess/C");

		//end the rules right after starting them
		for (int i = 0; i < rules.length; i++) {
			manager.beginRule(rules[i], null);
			status[0] = TestBarrier.STATUS_START;
			Thread t = new Thread(new RuleEnder(rules[i], status));
			t.start();
			TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);
			try {
				t.join();
			} catch (InterruptedException e1) {
				//ignore
			}
			//the thread should be dead now
			assertTrue("2." + i, !t.isAlive());
		}

		//try to end the rules when they are all started
		for (int i = 0; i < rules.length; i++) {
			status[0] = TestBarrier.STATUS_START;
			Thread t = new Thread(new RuleEnder(rules[i], status));
			t.start();
			TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);
			try {
				t.join();
			} catch (InterruptedException e1) {
				//ignore
			}
			//the thread should be dead now
			assertTrue("3." + i, !t.isAlive());
		}

		//try to end the rules after manager.endRule() has been called
		for (int i = rules.length; i > 0; i--) {
			manager.endRule(rules[i - 1]);
			status[0] = TestBarrier.STATUS_START;
			Thread t = new Thread(new RuleEnder(rules[i - 1], status));
			t.start();
			TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);
			try {
				t.join();
			} catch (InterruptedException e1) {
				//ignore
			}
			//the thread should be dead now
			assertTrue("4." + i, !t.isAlive());
		}
	}

	/**
	 * A job is running.  Wait until it is finished.
	 */
	private void waitForEnd(Job job) {
		int i = 0;
		while (job.getState() != Job.NONE) {
			Thread.yield();
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				//ignore
			}
			Thread.yield();
			//sanity test to avoid hanging tests
			assertTrue("Timeout waiting for job to end", i++ < 100);
		}
	}

	public void testIgnoreScheduleThreadJob() throws Exception {
		final int[] count = new int[1];
		JobChangeAdapter a = new JobChangeAdapter() {
			@Override
			public void running(org.eclipse.core.runtime.jobs.IJobChangeEvent event) {
				count[0]++;
			}
		};
		Job.getJobManager().addJobChangeListener(a);
		IdentityRule rule = new IdentityRule();
		try {
			Job.getJobManager().beginRule(rule, null);
			Job.getJobManager().currentJob().schedule();
		} finally {
			Job.getJobManager().endRule(rule);
		}
		Thread.sleep(250);
		Job.getJobManager().removeJobChangeListener(a);
		assertEquals("ThreadJob did not ignore reschedule", 0, count[0]);
	}

	public void testRunThreadJobIsNotRescheduled() throws Exception {
		final int[] count = new int[1];
		JobChangeAdapter a = new JobChangeAdapter() {
			@Override
			public void running(org.eclipse.core.runtime.jobs.IJobChangeEvent event) {
				count[0]++;
			}
		};
		Job.getJobManager().addJobChangeListener(a);
		IdentityRule rule = new IdentityRule();
		try {
			Job.getJobManager().beginRule(rule, null);
		} finally {
			Job.getJobManager().endRule(rule);
		}
		Thread.sleep(250);
		Job.getJobManager().removeJobChangeListener(a);
		assertEquals("ThreadJob did not ignore reschedule", 0, count[0]);
	}

	public void testRunNestedAcquireThreadIsNotRescheduled() throws Exception {
		final PathRule rule = new PathRule(getName());
		final PathRule subRule = new PathRule(getName() + "/subRule");

		final int[] count = new int[1];

		final Job job = new Job(getName() + "acquire") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					Job.getJobManager().beginRule(subRule, null);
				} finally {
					Job.getJobManager().endRule(subRule);
				}
				return Status.OK_STATUS;
			}
		};
		job.setRule(rule);

		JobChangeAdapter a = new JobChangeAdapter() {
			@Override
			public void running(org.eclipse.core.runtime.jobs.IJobChangeEvent event) {
				if (event.getJob() == job)
					return;
				count[0]++;
			}
		};
		Job.getJobManager().addJobChangeListener(a);
		job.schedule();
		Thread.sleep(250);
		Job.getJobManager().removeJobChangeListener(a);
		assertEquals("ThreadJob did not ignore reschedule", 0, count[0]);
	}

}
