/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.common; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.apache.commons.lang3.StringUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.common.error.FrameworkException; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.entity.AbstractNode; import org.structr.core.entity.SchemaNode; import org.structr.core.entity.SchemaProperty; import org.structr.core.entity.TestEight; import org.structr.core.entity.TestFive; import org.structr.core.entity.TestOne; import org.structr.core.entity.TestSix; import org.structr.core.entity.TestUser; import org.structr.core.graph.NodeAttribute; import org.structr.core.graph.NodeInterface; import org.structr.core.graph.Tx; import org.structr.core.property.IntProperty; import org.structr.core.property.StringProperty; import org.structr.core.script.Scripting; import org.structr.schema.action.ActionContext; /** * */ public class SystemTest extends StructrTest { private static final Logger logger = LoggerFactory.getLogger(SystemTest.class); @Test public void testCallbacksWithSuperUserContext() { final SecurityContext securityContext = SecurityContext.getSuperUserInstance(); try { testCallbacks(securityContext); } catch (FrameworkException fex) { fail("Unexpected exception"); } } @Test public void testCallbacksWithNormalContext() { try { TestUser person = this.createTestNode(TestUser.class); final SecurityContext securityContext = SecurityContext.getInstance(person, null, AccessMode.Backend); testCallbacks(securityContext); } catch (FrameworkException fex) { logger.warn("", fex); } } @Test public void testCallbackOrder() { try { // ##################################### test creation callbacks TestEight test = null; try (final Tx tx = app.tx()) { test = app.create(TestEight.class, new NodeAttribute(TestEight.testProperty, 123)); tx.success(); } // only the creation methods should have been called now! assertTrue("onCreationTimestamp should be != 0", test.getOnCreationTimestamp() != 0L); assertEquals("onModificationTimestamp should be == 0", 0L, test.getOnModificationTimestamp()); assertEquals("onDeletionTimestamp should be == 0", 0L, test.getOnDeletionTimestamp()); // only the creation methods should have been called now! assertTrue("afterCreationTimestamp should be != 0", test.getAfterCreationTimestamp() != 0L); assertEquals("afterModificationTimestamp should be == 0", 0L, test.getAfterModificationTimestamp()); // ##################################### test modification callbacks // reset timestamps test.resetTimestamps(); try (final Tx tx = app.tx()) { test.setProperty(TestEight.testProperty, 234); tx.success(); } // only the modification methods should have been called now! assertEquals("onCreationTimestamp should be == 0", 0L, test.getOnCreationTimestamp()); assertTrue("onModificationTimestamp should be != 0", test.getOnModificationTimestamp() != 0L); assertEquals("onDeletionTimestamp should be == 0", 0L, test.getOnDeletionTimestamp()); // only the modification methods should have been called now! assertEquals("afterCreationTimestamp should be == 0", 0L, test.getAfterCreationTimestamp()); assertTrue("afterModificationTimestamp should be != 0", test.getAfterModificationTimestamp() != 0L); // ##################################### test non-modifying set operation // reset timestamps test.resetTimestamps(); try (final Tx tx = app.tx()) { test.setProperty(TestEight.testProperty, 234); tx.success(); } // only the creation methods should have been called now! assertEquals("onCreationTimestamp should be == 0", 0L, test.getOnCreationTimestamp()); assertEquals("onModificationTimestamp should be == 0", 0L, test.getOnModificationTimestamp()); assertEquals("onDeletionTimestamp should be == 0", 0L, test.getOnDeletionTimestamp()); // only the creation methods should have been called now! assertEquals("afterCreationTimestamp should be == 0", 0L, test.getAfterCreationTimestamp()); assertEquals("afterModificationTimestamp should be == 0", 0L, test.getAfterModificationTimestamp()); // ##################################### test deletion // reset timestamps test.resetTimestamps(); try (final Tx tx = app.tx()) { app.delete(test); tx.success(); } // only the creation methods should have been called now! assertEquals("onCreationTimestamp should be == 0", 0L, test.getOnCreationTimestamp()); assertEquals("onModificationTimestamp should be == 0", 0L, test.getOnModificationTimestamp()); assertTrue("onDeletionTimestamp should be != 0", test.getOnDeletionTimestamp() != 0L); // only the creation methods should have been called now! assertEquals("afterCreationTimestamp should be == 0", 0L, test.getAfterCreationTimestamp()); assertEquals("afterModificationTimestamp should be == 0", 0L, test.getAfterModificationTimestamp()); } catch (FrameworkException ex) { logger.error("Error", ex); fail("Unexpected exception."); } } private void testCallbacks(final SecurityContext securityContext) throws FrameworkException { TestFive entity = null; Integer zero = 0; Integer one = 1; try (final Tx tx = app.tx()) { entity = app.create(TestFive.class); tx.success(); } catch (Throwable t) { logger.warn("", t); } assertNotNull("Entity should have been created", entity); // creation assertions try (final Tx tx = app.tx()) { assertEquals("modifiedInBeforeCreation should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInBeforeCreation)); assertEquals("modifiedInAfterCreation should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInAfterCreation)); // modification assertions assertEquals("modifiedInBeforeModification should have a value of 0: ", zero, entity.getProperty(TestFive.modifiedInBeforeModification)); assertEquals("modifiedInAfterModification should have a value of 0: ", zero, entity.getProperty(TestFive.modifiedInAfterModification)); } // 2nd part of the test: modify node try (final Tx tx = app.tx()) { final TestFive finalEntity = entity; finalEntity.setProperty(TestFive.intProperty, 123); tx.success(); } catch (Throwable t) { logger.warn("", t); } try (final Tx tx = app.tx()) { // creation assertions assertEquals("modifiedInBeforeCreation should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInBeforeCreation)); assertEquals("modifiedInAfterCreation should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInAfterCreation)); // modification assertions assertEquals("modifiedInBeforeModification should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInBeforeModification)); assertEquals("modifiedInAfterModification should have a value of 1: ", one, entity.getProperty(TestFive.modifiedInAfterModification)); } } /** * disabled, failing test to check for (existing, confirmed) flaw in parallel node instantiation) */ @Test public void testFlawedParallelInstantiation() { final int nodeCount = 1000; SchemaNode createTestType = null; // setup: create dynamic type with onCreate() method try (final Tx tx = app.tx()) { createTestType = createTestNode(SchemaNode.class, "CreateTest"); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } Class testType = StructrApp.getConfiguration().getNodeEntityClass("CreateTest"); assertNotNull("Type CreateTest should have been created", testType); // second step: create 1000 test nodes try (final Tx tx = app.tx()) { createTestNodes(testType, nodeCount); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } try (final Tx tx = app.tx()) { createTestType.setProperty(new StringProperty("testCount"), "Integer"); createTestType.setProperty(new StringProperty("___onCreate"), "set(this, 'testCount', size(find('CreateTest')))"); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } testType = StructrApp.getConfiguration().getNodeEntityClass("CreateTest"); NodeInterface node = null; // third step: create a single node in a separate transaction try (final Tx tx = app.tx()) { node = createTestNode(testType, "Tester"); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } // fourth step: check property value try (final Tx tx = app.tx()) { final Integer testCount = node.getProperty(new IntProperty("testCount")); assertEquals("Invalid node count, check parallel instantiation!", (int)nodeCount+1, (int)testCount); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } } @Test public void testRollbackOnError () { final ActionContext ctx = new ActionContext(securityContext, null); /** * first the old scripting style */ TestOne testNode = null; try (final Tx tx = app.tx()) { testNode = createTestNode(TestOne.class); testNode.setProperty(TestOne.aString, "InitialString"); testNode.setProperty(TestOne.anInt, 42); tx.success(); } catch (FrameworkException ex) { logger.warn("", ex); fail("Unexpected exception"); } try (final Tx tx = app.tx()) { Scripting.replaceVariables(ctx, testNode, "${ ( set(this, 'aString', 'NewString'), set(this, 'anInt', 'NOT_AN_INTEGER') ) }"); fail("StructrScript: setting anInt to 'NOT_AN_INTEGER' should cause an Exception"); tx.success(); } catch (FrameworkException expected) { } try { try (final Tx tx = app.tx()) { assertEquals("Property value should still have initial value!", "InitialString", testNode.getProperty(TestOne.aString)); tx.success(); } } catch (FrameworkException ex) { logger.warn("", ex); fail("Unexpected exception"); } } @Test public void testConstraintsConcurrently() { /** * This test concurrently creates 1000 nodes in * batches of 10, with 10 threads simultaneously. */ try (final Tx tx = app.tx()) { app.create(SchemaNode.class, new NodeAttribute(SchemaNode.name, "Item"), new NodeAttribute(SchemaNode.schemaProperties, Arrays.asList(app.create( SchemaProperty.class, new NodeAttribute(SchemaProperty.name, "name"), new NodeAttribute(SchemaProperty.propertyType, "String"), new NodeAttribute(SchemaProperty.unique, true), new NodeAttribute(SchemaProperty.indexed, true) ) )) ); tx.success(); } catch (FrameworkException ex) { fail("Error creating schema node"); } final Class itemType = StructrApp.getConfiguration().getNodeEntityClass("Item"); assertNotNull("Error creating schema node", itemType); final Runnable worker = new Runnable() { @Override public void run() { int i = 0; while (i < 1000) { try (final Tx tx = app.tx()) { for (int j=0; j<10 && i<1000; j++) { app.create(itemType, "Item" + StringUtils.leftPad(Integer.toString(i++), 5, "0")); } tx.success(); } catch (FrameworkException expected) { } } } }; final ExecutorService service = Executors.newFixedThreadPool(10); final List<Future> futures = new LinkedList<>(); for (int i=0; i<10; i++) { futures.add(service.submit(worker)); } // wait for result of async. operations for (final Future future : futures) { try { future.get(); } catch (Throwable t) { logger.warn("", t); fail("Unexpected exception"); } } try (final Tx tx = app.tx()) { final List<NodeInterface> items = app.nodeQuery(itemType).sort(AbstractNode.name).getAsList(); int i = 0; assertEquals("Invalid concurrent constraint test result", 1000, items.size()); for (final NodeInterface item : items) { assertEquals("Invalid name detected", "Item" + StringUtils.leftPad(Integer.toString(i++), 5, "0"), item.getName()); } tx.success(); } catch (FrameworkException ex) { fail("Unexpected exception"); } service.shutdownNow(); } @Test public void testTransactionIsolation() { // Tests the transaction isolation of the underlying database layer. // Create a node and use many different threads to set a property on // it in a transaction. Observe the property value to check that the // threads do not interfere with each other. try { final TestOne test = createTestNode(TestOne.class); final ExecutorService executor = Executors.newCachedThreadPool(); final List<TestRunner> tests = new LinkedList<>(); final List<Future> futures = new LinkedList<>(); // create and run test runners for (int i=0; i<25; i++) { final TestRunner runner = new TestRunner(app, test); futures.add(executor.submit(runner)); tests.add(runner); } // wait for termination for (final Future future : futures) { future.get(); System.out.print("."); } System.out.println(); // check for success for (final TestRunner runner : tests) { assertTrue("Could not validate transaction isolation", runner.success()); } executor.shutdownNow(); } catch (Throwable fex) { fail("Unexpected exception"); } } @Test public void testTransactionIsolationWithFailures() { // Tests the transaction isolation of the underlying database layer. // Create a node and use ten different threads to set a property on // it in a transaction. Observe the property value to check that the // threads do not interfere with each other. try { final TestOne test = createTestNode(TestOne.class); final ExecutorService executor = Executors.newCachedThreadPool(); final List<FailingTestRunner> tests = new LinkedList<>(); final List<Future> futures = new LinkedList<>(); // create and run test runners for (int i=0; i<25; i++) { final FailingTestRunner runner = new FailingTestRunner(app, test); futures.add(executor.submit(runner)); tests.add(runner); } // wait for termination for (final Future future : futures) { future.get(); System.out.print("."); } System.out.println(); // check for success for (final FailingTestRunner runner : tests) { assertTrue("Could not validate transaction isolation", runner.success()); } executor.shutdownNow(); } catch (Throwable fex) { fail("Unexpected exception"); } } @Test public void testConcurrentIdenticalRelationshipCreation() { try { final ExecutorService service = Executors.newCachedThreadPool(); final TestSix source = createTestNode(TestSix.class); final TestOne target = createTestNode(TestOne.class); final Future one = service.submit(new RelationshipCreator(source, target)); final Future two = service.submit(new RelationshipCreator(source, target)); // wait for completion one.get(); two.get(); try (final Tx tx = app.tx()) { // check for a single relationship since all three parts of // both relationships are equal => only one should be created final List<TestOne> list = source.getProperty(TestSix.oneToManyTestOnes); assertEquals("Invalid concurrent identical relationship creation result", 1, list.size()); tx.success(); } service.shutdownNow(); } catch (ExecutionException | InterruptedException | FrameworkException fex) { logger.warn("", fex); } } private static class TestRunner implements Runnable { private boolean success = true; private TestOne test = null; private App app = null; public TestRunner(final App app, final TestOne test) { this.app = app; this.test = test; } public boolean success() { return success; } @Override public void run() { final String name = Thread.currentThread().getName(); try (final Tx tx = app.tx()) { // set property on node test.setProperty(TestOne.name, name); for (int i=0; i<100; i++) { // wait some time try { Thread.sleep(1); } catch (Throwable t) {} // check if the given name is still there final String testName = test.getProperty(TestOne.name); if (!name.equals(testName)) { success = false; } } tx.success(); } catch (Throwable t) { success = false; } } } private static class FailingTestRunner implements Runnable { private boolean success = true; private TestOne test = null; private App app = null; public FailingTestRunner(final App app, final TestOne test) { this.app = app; this.test = test; } public boolean success() { return success; } @Override public void run() { final String name = Thread.currentThread().getName(); try (final Tx tx = app.tx()) { // set property on node test.setProperty(TestOne.name, name); for (int i=0; i<100; i++) { // wait some time try { Thread.sleep(1); } catch (Throwable t) {} // check if the given name is still there final String testName = test.getProperty(TestOne.name); if (!name.equals(testName)) { success = false; } } // make Transactions fail randomly if (Math.random() <= 0.5) { tx.success(); } } catch (Throwable t) { success = false; } } } private static class RelationshipCreator implements Runnable { private TestSix source = null; private TestOne target = null; public RelationshipCreator(final TestSix source, final TestOne target) { this.source = source; this.target = target; } @Override public void run() { try (final Tx tx = StructrApp.getInstance().tx()) { final List<TestOne> list = new LinkedList<>(); list.add(target); source.setProperty(TestSix.oneToManyTestOnes, list); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } } }