content: /*******************************************************************************
   * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *    Marc R. Hoffmann - initial API and implementation
   *    
   *******************************************************************************/
  package org.jacoco.core.test.validation.java5;
  
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertTrue;
  
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  
  import org.jacoco.core.instr.Instrumenter;
  import org.jacoco.core.internal.instr.InstrSupport;
  import org.jacoco.core.runtime.IRuntime;
  import org.jacoco.core.runtime.SystemPropertiesRuntime;
  import org.jacoco.core.test.TargetLoader;
  import org.jacoco.core.test.validation.java5.targets.StructuredLockingTarget;
  import org.junit.Test;
  import org.objectweb.asm.Opcodes;
  import org.objectweb.asm.tree.AbstractInsnNode;
  import org.objectweb.asm.tree.ClassNode;
  import org.objectweb.asm.tree.MethodNode;
  import org.objectweb.asm.tree.TryCatchBlockNode;
  import org.objectweb.asm.tree.VarInsnNode;
  import org.objectweb.asm.tree.analysis.Analyzer;
  import org.objectweb.asm.tree.analysis.AnalyzerException;
  import org.objectweb.asm.tree.analysis.BasicInterpreter;
  import org.objectweb.asm.tree.analysis.BasicValue;
  import org.objectweb.asm.tree.analysis.Frame;
  import org.objectweb.asm.tree.analysis.Interpreter;
  
  /**
   * Tests that the invariants specified in <a href=
   * "https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-2.html#jvms-2.11.10">chapter
   * 2.11.10 of the JVM Spec</a> do also hold for instrumented classes.
   * 
   * This is important because JIT compiler in HotSpot JVM ignores methods with
   * unstructured locking, so that they executed by interpreter. Android Runtime
   * also doesn't optimize such methods.
   *
   * TODO verification implemented here is incomplete - in particular it is unable
   * to catch problem described in https://github.com/jacoco/jacoco/issues/626
   */
  public class StructuredLockingTest {
  
  	@Test
  	public void testMonitorExit() throws Exception {
  		assertStructuredLocking(TargetLoader
  				.getClassDataAsBytes(StructuredLockingTarget.class));
  	}
  
  	private void assertStructuredLocking(byte[] source) throws Exception {
  		IRuntime runtime = new SystemPropertiesRuntime();
  		Instrumenter instrumenter = new Instrumenter(runtime);
  		byte[] instrumented = instrumenter.instrument(source, "TestTarget");
  
  		ClassNode cn = new ClassNode();
  		InstrSupport.classReaderFor(instrumented).accept(cn, 0);
  		for (MethodNode mn : cn.methods) {
  			assertStructuredLocking(cn.name, mn);
  		}
  	}
  
  	private void assertStructuredLocking(String owner, MethodNode mn)
  			throws Exception {
  		Analyzer<BasicValue> analyzer = new Analyzer<BasicValue>(
  				new BasicInterpreter()) {
  
  			@Override
  			protected Frame<BasicValue> newFrame(int nLocals, int nStack) {
  				return new LockFrame(nLocals, nStack);
  			}
  
  			@Override
  			protected Frame<BasicValue> newFrame(
  					Frame<? extends BasicValue> src) {
  				return new LockFrame(src);
  			}
  		};
  
  		Frame<BasicValue>[] frames = analyzer.analyze(owner, mn);
  
  		// Make sure no locks are left when method exits:
  		for (int i = 0; i < frames.length; i++) {
  			AbstractInsnNode insn = mn.instructions.get(i);
  			switch (insn.getOpcode()) {
  			case Opcodes.IRETURN:
  			case Opcodes.LRETURN:
  			case Opcodes.FRETURN:
  			case Opcodes.DRETURN:
  			case Opcodes.ARETURN:
  			case Opcodes.RETURN:
  				((LockFrame) frames[i]).assertNoLock("Exit with lock");
  				break;
  			case Opcodes.ATHROW:
  				List<TryCatchBlockNode> handlers = analyzer.getHandlers(i);
  				if (handlers == null || handlers.isEmpty()) {
  					((LockFrame) frames[i]).assertNoLock("Exit with lock");
  				}
  				break;
  			}
  		}
  
  		// Only instructions protected by a catch-all handler can hold locks:
  		for (int i = 0; i < frames.length; i++) {
  			AbstractInsnNode insn = mn.instructions.get(i);
  			if (insn.getOpcode() > 0) {
  				boolean catchAll = false;
  				List<TryCatchBlockNode> handlers = analyzer.getHandlers(i);
  				if (handlers != null) {
  					for (TryCatchBlockNode node : handlers) {
  						catchAll |= node.type == null;
  					}
  				}
  				if (!catchAll) {
  					((LockFrame) frames[i])
  							.assertNoLock("No handlers for insn with lock");
  				}
  			}
  		}
  
  	}
  
  	/**
  	 * A Frame implementation that keeps track of the locking state. It is
  	 * assumed that the monitor objects are stored in local variables.
  	 */
  	private static class LockFrame extends Frame<BasicValue> {
  
  		Set<Integer> locks;
  
  		public LockFrame(final int nLocals, final int nStack) {
  			super(nLocals, nStack);
  			locks = new HashSet<Integer>();
  		}
  
  		public LockFrame(Frame<? extends BasicValue> src) {
  			super(src);
  		}
  
  		@Override
  		public Frame<BasicValue> init(Frame<? extends BasicValue> src) {
  			locks = new HashSet<Integer>(((LockFrame) src).locks);
  			return super.init(src);
  		}
  
  		@Override
  		public void execute(AbstractInsnNode insn,
  				Interpreter<BasicValue> interpreter) throws AnalyzerException {
  			super.execute(insn, interpreter);
  			switch (insn.getOpcode()) {
  			case Opcodes.MONITORENTER:
  				// Lock is stored in a local variable:
  				enter(((VarInsnNode) insn.getPrevious()).var);
  				break;
  			case Opcodes.MONITOREXIT:
  				// Lock is stored in a local variable:
  				exit(((VarInsnNode) insn.getPrevious()).var);
  				break;
  			}
  		}
  
  		void enter(int lock) {
  			assertTrue("multiple ENTER for lock " + lock,
  					locks.add(Integer.valueOf(lock)));
  		}
  
  		void exit(int lock) {
  			assertTrue("invalid EXIT for lock " + lock,
  					locks.remove(Integer.valueOf(lock)));
  		}
  
  		@Override
  		public boolean merge(Frame<? extends BasicValue> frame,
  				Interpreter<BasicValue> interpreter) throws AnalyzerException {
  			this.locks.addAll(((LockFrame) frame).locks);
  			return super.merge(frame, interpreter);
  		}
  
  		void assertNoLock(String message) {
  			assertEquals(message, Collections.emptySet(), locks);
  
  		}
  	}
  
  }
  