/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Victor Olaya (Boundless) - initial implementation */ package org.locationtech.geogig.test.integration; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.io.File; import java.util.ServiceLoader; import org.junit.Test; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.hooks.CannotRunGeogigOperationException; import org.locationtech.geogig.api.hooks.CommandHook; import org.locationtech.geogig.api.hooks.Scripting; import org.locationtech.geogig.api.porcelain.AddOp; import org.locationtech.geogig.api.porcelain.CommitOp; import com.google.common.base.Charsets; import com.google.common.io.Files; public class HooksTest extends RepositoryTestCase { @Override protected void setUpInternal() throws Exception { File hooksFolder = new File(geogig.getPlatform().pwd(), ".geogig/hooks"); File[] files = hooksFolder.listFiles(); for (File file : files) { file.delete(); assertFalse(file.exists()); } } @Test public void testHookWithError() throws Exception { CharSequence wrongHookCode = "this is a syntactically wrong sentence"; File hooksFolder = new File(geogig.getPlatform().pwd(), ".geogig/hooks"); File commitPreHookFile = new File(hooksFolder, "pre_commit.js"); Files.write(wrongHookCode, commitPreHookFile, Charsets.UTF_8); insertAndAdd(points1); try { geogig.command(CommitOp.class).setMessage("A message").call(); fail(); } catch (Exception e) { assertTrue(true); } } @Test public void testHook() throws Exception { // a hook that only accepts commit messages longer with at least 4 words, and converts // message to lower case CharSequence commitPreHookCode = "exception = Packages.org.locationtech.geogig.api.hooks.CannotRunGeogigOperationException;\n" + "msg = params.get(\"message\");\n" + "if (msg.length() < 30){\n" + "\tthrow new exception(\"Commit messages must have at least 30 characters\");\n}" + "params.put(\"message\", msg.toLowerCase());"; File hooksFolder = new File(geogig.getPlatform().pwd(), ".geogig/hooks"); File commitPreHookFile = new File(hooksFolder, "pre_commit.js"); Files.write(commitPreHookCode, commitPreHookFile, Charsets.UTF_8); insertAndAdd(points1); try { geogig.command(CommitOp.class).setMessage("A short message").call(); fail(); } catch (Exception e) { String javaVersion = System.getProperty("java.version"); // Rhino in jdk6 throws a different exception if (javaVersion.startsWith("1.6")) { String expected = "Script " + commitPreHookFile + " threw an exception"; assertTrue(e.getMessage(), e.getMessage().contains(expected)); } else { assertTrue( e.getMessage(), e.getMessage().startsWith( "Commit messages must have at least 30 characters")); } } String longMessage = "THIS IS A LONG UPPERCASE COMMIT MESSAGE"; RevCommit commit = geogig.command(CommitOp.class).setMessage(longMessage).call(); assertEquals(longMessage.toLowerCase(), commit.getMessage()); } @Test public void testExecutableScriptFileHook() throws Exception { File hooksFolder = new File(geogig.getPlatform().pwd(), ".geogig/hooks"); File commitPreHookFile; String commitPreHookCode; // a hook that returns non-zero if (Scripting.isWindows()) { commitPreHookCode = "exit 1"; } else { commitPreHookCode = "#!/bin/sh\nexit 1"; } commitPreHookFile = new File(hooksFolder, "pre_commit.bat"); Files.write(commitPreHookCode, commitPreHookFile, Charsets.UTF_8); commitPreHookFile.setExecutable(true); insertAndAdd(points1); try { geogig.command(CommitOp.class).setMessage("Message").call(); fail(); } catch (Exception e) { assertTrue(e instanceof CannotRunGeogigOperationException); } // a hook that returns zero if (Scripting.isWindows()) { commitPreHookCode = "exit 0"; } else { commitPreHookCode = "#!/bin/sh\nexit 0"; } commitPreHookFile = new File(hooksFolder, "pre_commit.bat"); Files.write(commitPreHookCode, commitPreHookFile, Charsets.UTF_8); commitPreHookFile.setExecutable(true); geogig.command(CommitOp.class).setMessage("Message").call(); } @Test public void testFailingPostPostProcessHook() throws Exception { CharSequence postHookCode = "exception = Packages.org.locationtech.geogig.api.hooks.CannotRunGeogigOperationException;\n" + "throw new exception();"; File hooksFolder = new File(geogig.getPlatform().pwd(), ".geogig/hooks"); File commitPostHookFile = new File(hooksFolder, "post_commit.js"); Files.write(postHookCode, commitPostHookFile, Charsets.UTF_8); insertAndAdd(points1); geogig.command(CommitOp.class).setMessage("A message").call(); } @Test public void testClasspathHook() throws Exception { ClasspathHookTest.ENABLED = true; try { ClasspathHookTest.CAPTURE_CLASS = AddOp.class; insertAndAdd(points1); assertEquals(AddOp.class, ClasspathHookTest.PRE_OP.getClass()); assertEquals(AddOp.class, ClasspathHookTest.POST_OP.getClass()); } finally { ClasspathHookTest.reset(); } } @Test public void testClasspathHookPreFail() throws Exception { ClasspathHookTest.ENABLED = true; try { ClasspathHookTest.PRE_FAIL = true; ClasspathHookTest.CAPTURE_CLASS = AddOp.class; try { insertAndAdd(points1); fail("Expected pre hook exception"); } catch (CannotRunGeogigOperationException e) { assertEquals("expected", e.getMessage()); } assertEquals(AddOp.class, ClasspathHookTest.PRE_OP.getClass()); assertEquals(AddOp.class, ClasspathHookTest.PRE_OP.getClass()); } finally { ClasspathHookTest.reset(); } } @Test public void testClasspathHookPostFail() throws Exception { ClasspathHookTest.ENABLED = true; try { ClasspathHookTest.POST_FAIL = true; ClasspathHookTest.CAPTURE_CLASS = AddOp.class; insertAndAdd(points1); // post hook errors should not forbid the operation to return successfully assertTrue(ClasspathHookTest.POST_EXCEPTION_THROWN); assertEquals(AddOp.class, ClasspathHookTest.PRE_OP.getClass()); assertEquals(AddOp.class, ClasspathHookTest.PRE_OP.getClass()); } finally { ClasspathHookTest.reset(); } } /** * This command hook is discoverable through the {@link ServiceLoader} SPI as there's a * {@code org.locationtech.geogig.api.hooks.CommandHook} file in * {@code src/test/resources/META-INF/services} but the static ENABLED flag must be set by the * test case for it to be run. */ public static final class ClasspathHookTest implements CommandHook { private static boolean ENABLED = false; private static boolean PRE_FAIL = false; private static boolean POST_FAIL = false; private static boolean POST_EXCEPTION_THROWN; @SuppressWarnings("rawtypes") private static Class<? extends AbstractGeoGigOp> CAPTURE_CLASS; private static AbstractGeoGigOp<?> PRE_OP, POST_OP; public ClasspathHookTest() { reset(); } private static void reset() { ENABLED = false; PRE_FAIL = false; POST_FAIL = false; CAPTURE_CLASS = AbstractGeoGigOp.class; PRE_OP = null; POST_OP = null; POST_EXCEPTION_THROWN = false; } @SuppressWarnings("rawtypes") @Override public boolean appliesTo(Class<? extends AbstractGeoGigOp<?>> clazz) { boolean enabled = ENABLED; Class<? extends AbstractGeoGigOp> captureClass = CAPTURE_CLASS; checkNotNull(clazz); checkNotNull(captureClass); boolean applies = enabled && CAPTURE_CLASS.equals(clazz); return applies; } @Override public <C extends AbstractGeoGigOp<?>> C pre(C command) throws CannotRunGeogigOperationException { checkState(ENABLED); PRE_OP = command; if (PRE_FAIL) { throw new CannotRunGeogigOperationException("expected"); } return command; } @SuppressWarnings("unchecked") @Override public <T> T post(AbstractGeoGigOp<T> command, Object retVal, RuntimeException exception) throws Exception { checkState(ENABLED); POST_OP = command; if (POST_FAIL) { POST_EXCEPTION_THROWN = true; throw new RuntimeException("expected"); } return (T) retVal; } } }