diff --git a/.gitignore b/.gitignore index f48e37303..e4536bc1b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,12 @@ transaction.log .floo .flooignore gh-pages + +# Agents +.claude +.vscode + +# Beads / Dolt files (added by bd init) +.dolt/ +*.db +.beads-credential-key diff --git a/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerControlListenerAdaptor.java b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerControlListenerAdaptor.java new file mode 100644 index 000000000..584bdcfd2 --- /dev/null +++ b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerControlListenerAdaptor.java @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import org.jboss.arquillian.container.spi.ContainerControlListener; +import org.jboss.arquillian.container.spi.event.DeployDeployment; +import org.jboss.arquillian.container.spi.event.KillContainer; +import org.jboss.arquillian.container.spi.event.SetupContainer; +import org.jboss.arquillian.container.spi.event.StartContainer; +import org.jboss.arquillian.container.spi.event.StopContainer; +import org.jboss.arquillian.container.spi.event.UnDeployDeployment; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.Manager; + +/** + * Adaptor that bridges single-container control events to the {@link ContainerControlListener} SPI. + * + * @author Arquillian + */ +public class ContainerControlListenerAdaptor { + + @Inject + private Instance manager; + + public void onSetupContainer(@Observes SetupContainer event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.setupContainer(event.getContainer()); + } + } + + public void onStartContainer(@Observes StartContainer event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.startContainer(event.getContainer()); + } + } + + public void onStopContainer(@Observes StopContainer event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.stopContainer(event.getContainer()); + } + } + + public void onKillContainer(@Observes KillContainer event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.killContainer(event.getContainer()); + } + } + + public void onDeployDeployment(@Observes DeployDeployment event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.deployDeployment(event.getContainer(), event.getDeployment()); + } + } + + public void onUndeployDeployment(@Observes UnDeployDeployment event) throws Exception { + for (ContainerControlListener listener : manager.get().getListeners(ContainerControlListener.class)) { + listener.undeployDeployment(event.getContainer(), event.getDeployment()); + } + } +} diff --git a/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerExtension.java b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerExtension.java index 6375edb35..6fba7e3da 100644 --- a/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerExtension.java +++ b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerExtension.java @@ -27,6 +27,8 @@ import org.jboss.arquillian.container.impl.context.DeploymentContextImpl; import org.jboss.arquillian.core.spi.LoadableExtension; + + /** * ContainerExtension * @@ -43,6 +45,9 @@ public void register(ExtensionBuilder builder) { .observer(ContainerLifecycleController.class) .observer(ContainerDeployController.class) .observer(ArchiveDeploymentExporter.class) - .observer(DeploymentExceptionHandler.class); + .observer(DeploymentExceptionHandler.class) + .observer(ContainerLifecycleListenerAdaptor.class) + .observer(ContainerControlListenerAdaptor.class) + .observer(ContainerMultiControlListenerAdaptor.class); } } diff --git a/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerAdaptor.java b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerAdaptor.java new file mode 100644 index 000000000..d4918d038 --- /dev/null +++ b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerAdaptor.java @@ -0,0 +1,119 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import org.jboss.arquillian.container.spi.ContainerLifecycleListener; +import org.jboss.arquillian.container.spi.event.container.AfterDeploy; +import org.jboss.arquillian.container.spi.event.container.AfterKill; +import org.jboss.arquillian.container.spi.event.container.AfterSetup; +import org.jboss.arquillian.container.spi.event.container.AfterStart; +import org.jboss.arquillian.container.spi.event.container.AfterStop; +import org.jboss.arquillian.container.spi.event.container.AfterUnDeploy; +import org.jboss.arquillian.container.spi.event.container.BeforeDeploy; +import org.jboss.arquillian.container.spi.event.container.BeforeKill; +import org.jboss.arquillian.container.spi.event.container.BeforeSetup; +import org.jboss.arquillian.container.spi.event.container.BeforeStart; +import org.jboss.arquillian.container.spi.event.container.BeforeStop; +import org.jboss.arquillian.container.spi.event.container.BeforeUnDeploy; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.Manager; + +/** + * Adaptor that bridges per-container notification events to the {@link ContainerLifecycleListener} SPI. + * + * @author Arquillian + */ +public class ContainerLifecycleListenerAdaptor { + + @Inject + private Instance manager; + + public void onBeforeSetup(@Observes BeforeSetup event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeSetup(event.getDeployableContainer()); + } + } + + public void onAfterSetup(@Observes AfterSetup event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterSetup(event.getDeployableContainer()); + } + } + + public void onBeforeStart(@Observes BeforeStart event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeStart(event.getDeployableContainer()); + } + } + + public void onAfterStart(@Observes AfterStart event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterStart(event.getDeployableContainer()); + } + } + + public void onBeforeStop(@Observes BeforeStop event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeStop(event.getDeployableContainer()); + } + } + + public void onAfterStop(@Observes AfterStop event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterStop(event.getDeployableContainer()); + } + } + + public void onBeforeKill(@Observes BeforeKill event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeKill(event.getDeployableContainer()); + } + } + + public void onAfterKill(@Observes AfterKill event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterKill(event.getDeployableContainer()); + } + } + + public void onBeforeDeploy(@Observes BeforeDeploy event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeDeploy(event.getDeployableContainer(), event.getDeployment()); + } + } + + public void onAfterDeploy(@Observes AfterDeploy event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterDeploy(event.getDeployableContainer(), event.getDeployment()); + } + } + + public void onBeforeUndeploy(@Observes BeforeUnDeploy event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.beforeUndeploy(event.getDeployableContainer(), event.getDeployment()); + } + } + + public void onAfterUndeploy(@Observes AfterUnDeploy event) throws Exception { + for (ContainerLifecycleListener listener : manager.get().getListeners(ContainerLifecycleListener.class)) { + listener.afterUndeploy(event.getDeployableContainer(), event.getDeployment()); + } + } +} diff --git a/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerAdaptor.java b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerAdaptor.java new file mode 100644 index 000000000..5d80270d7 --- /dev/null +++ b/container/impl-base/src/main/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerAdaptor.java @@ -0,0 +1,91 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import org.jboss.arquillian.container.spi.ContainerMultiControlListener; +import org.jboss.arquillian.container.spi.event.DeployManagedDeployments; +import org.jboss.arquillian.container.spi.event.SetupContainers; +import org.jboss.arquillian.container.spi.event.StartClassContainers; +import org.jboss.arquillian.container.spi.event.StartSuiteContainers; +import org.jboss.arquillian.container.spi.event.StopClassContainers; +import org.jboss.arquillian.container.spi.event.StopManualContainers; +import org.jboss.arquillian.container.spi.event.StopSuiteContainers; +import org.jboss.arquillian.container.spi.event.UnDeployManagedDeployments; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.Manager; + +/** + * Adaptor that bridges multi-container control events to the {@link ContainerMultiControlListener} SPI. + * + * @author Arquillian + */ +public class ContainerMultiControlListenerAdaptor { + + @Inject + private Instance manager; + + public void onSetupContainers(@Observes SetupContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.setupContainers(); + } + } + + public void onStartSuiteContainers(@Observes StartSuiteContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.startSuiteContainers(); + } + } + + public void onStartClassContainers(@Observes StartClassContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.startClassContainers(); + } + } + + public void onStopSuiteContainers(@Observes StopSuiteContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.stopSuiteContainers(); + } + } + + public void onStopClassContainers(@Observes StopClassContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.stopClassContainers(); + } + } + + public void onStopManualContainers(@Observes StopManualContainers event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.stopManualContainers(); + } + } + + public void onDeployManagedDeployments(@Observes DeployManagedDeployments event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.deployManagedDeployments(); + } + } + + public void onUndeployManagedDeployments(@Observes UnDeployManagedDeployments event) throws Exception { + for (ContainerMultiControlListener listener : manager.get().getListeners(ContainerMultiControlListener.class)) { + listener.undeployManagedDeployments(); + } + } +} diff --git a/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerControlListenerTestCase.java b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerControlListenerTestCase.java new file mode 100644 index 000000000..eb7b54c72 --- /dev/null +++ b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerControlListenerTestCase.java @@ -0,0 +1,165 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import java.util.List; +import org.jboss.arquillian.container.spi.Container; +import org.jboss.arquillian.container.spi.ContainerControlListener; +import org.jboss.arquillian.container.spi.client.deployment.Deployment; +import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; +import org.jboss.arquillian.container.spi.event.DeployDeployment; +import org.jboss.arquillian.container.spi.event.KillContainer; +import org.jboss.arquillian.container.spi.event.SetupContainer; +import org.jboss.arquillian.container.spi.event.StartContainer; +import org.jboss.arquillian.container.spi.event.StopContainer; +import org.jboss.arquillian.container.spi.event.UnDeployDeployment; +import org.jboss.arquillian.container.test.AbstractContainerTestBase; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Verifies that {@link ContainerControlListener} instances registered via + * {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)} are notified + * for single-container control events. + */ +@RunWith(MockitoJUnitRunner.class) +public class ContainerControlListenerTestCase extends AbstractContainerTestBase { + + @Mock + private Container container; + + @Override + protected void addExtensions(List> extensions) { + extensions.add(ContainerControlListenerAdaptor.class); + } + + @Test + public void shouldNotifyListenerOnSetupContainer() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + fire(new SetupContainer(container)); + Assert.assertTrue("setupContainer() should have been called", listener.setupContainer); + Assert.assertSame(container, listener.container); + } + + @Test + public void shouldNotifyListenerOnStartContainer() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + fire(new StartContainer(container)); + Assert.assertTrue("startContainer() should have been called", listener.startContainer); + } + + @Test + public void shouldNotifyListenerOnStopContainer() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + fire(new StopContainer(container)); + Assert.assertTrue("stopContainer() should have been called", listener.stopContainer); + } + + @Test + public void shouldNotifyListenerOnKillContainer() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + fire(new KillContainer(container)); + Assert.assertTrue("killContainer() should have been called", listener.killContainer); + } + + @Test + public void shouldNotifyListenerOnDeployDeployment() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + DeploymentDescription description = + new DeploymentDescription("test", ShrinkWrap.create(JavaArchive.class)); + Deployment deployment = new Deployment(description); + + fire(new DeployDeployment(container, deployment)); + Assert.assertTrue("deployDeployment() should have been called", listener.deployDeployment); + Assert.assertSame(container, listener.deployContainer); + Assert.assertSame(deployment, listener.deployment); + } + + @Test + public void shouldNotifyListenerOnUndeployDeployment() throws Exception { + TrackingContainerControlListener listener = new TrackingContainerControlListener(); + getManager().addListener(ContainerControlListener.class, listener); + + DeploymentDescription description = + new DeploymentDescription("test", ShrinkWrap.create(JavaArchive.class)); + Deployment deployment = new Deployment(description); + + fire(new UnDeployDeployment(container, deployment)); + Assert.assertTrue("undeployDeployment() should have been called", listener.undeployDeployment); + } + + private static class TrackingContainerControlListener implements ContainerControlListener { + boolean setupContainer = false; + boolean startContainer = false; + boolean stopContainer = false; + boolean killContainer = false; + boolean deployDeployment = false; + boolean undeployDeployment = false; + Container container = null; + Container deployContainer = null; + Deployment deployment = null; + + @Override + public void setupContainer(Container c) throws Exception { + setupContainer = true; + container = c; + } + + @Override + public void startContainer(Container c) throws Exception { + startContainer = true; + } + + @Override + public void stopContainer(Container c) throws Exception { + stopContainer = true; + } + + @Override + public void killContainer(Container c) throws Exception { + killContainer = true; + } + + @Override + public void deployDeployment(Container c, Deployment d) throws Exception { + deployDeployment = true; + deployContainer = c; + deployment = d; + } + + @Override + public void undeployDeployment(Container c, Deployment d) throws Exception { + undeployDeployment = true; + } + } +} diff --git a/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerTestCase.java b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerTestCase.java new file mode 100644 index 000000000..98599ab90 --- /dev/null +++ b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerLifecycleListenerTestCase.java @@ -0,0 +1,220 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import java.util.List; +import org.jboss.arquillian.container.spi.ContainerLifecycleListener; +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; +import org.jboss.arquillian.container.spi.event.container.AfterDeploy; +import org.jboss.arquillian.container.spi.event.container.AfterKill; +import org.jboss.arquillian.container.spi.event.container.AfterSetup; +import org.jboss.arquillian.container.spi.event.container.AfterStart; +import org.jboss.arquillian.container.spi.event.container.AfterStop; +import org.jboss.arquillian.container.spi.event.container.AfterUnDeploy; +import org.jboss.arquillian.container.spi.event.container.BeforeDeploy; +import org.jboss.arquillian.container.spi.event.container.BeforeKill; +import org.jboss.arquillian.container.spi.event.container.BeforeSetup; +import org.jboss.arquillian.container.spi.event.container.BeforeStart; +import org.jboss.arquillian.container.spi.event.container.BeforeStop; +import org.jboss.arquillian.container.spi.event.container.BeforeUnDeploy; +import org.jboss.arquillian.container.test.AbstractContainerTestBase; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Verifies that {@link ContainerLifecycleListener} instances registered via + * {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)} are notified + * for container notification events. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +@RunWith(MockitoJUnitRunner.class) +public class ContainerLifecycleListenerTestCase extends AbstractContainerTestBase { + + @Mock + private DeployableContainer deployableContainer; + + @Override + protected void addExtensions(List> extensions) { + extensions.add(ContainerLifecycleListenerAdaptor.class); + } + + @Test + public void shouldNotifyListenerOnSetupEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + fire(new BeforeSetup(deployableContainer)); + Assert.assertTrue("beforeSetup() should have been called", listener.beforeSetup); + Assert.assertSame(deployableContainer, listener.deployableContainer); + + fire(new AfterSetup(deployableContainer)); + Assert.assertTrue("afterSetup() should have been called", listener.afterSetup); + } + + @Test + public void shouldNotifyListenerOnStartEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + fire(new BeforeStart(deployableContainer)); + Assert.assertTrue("beforeStart() should have been called", listener.beforeStart); + + fire(new AfterStart(deployableContainer)); + Assert.assertTrue("afterStart() should have been called", listener.afterStart); + } + + @Test + public void shouldNotifyListenerOnStopEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + fire(new BeforeStop(deployableContainer)); + Assert.assertTrue("beforeStop() should have been called", listener.beforeStop); + + fire(new AfterStop(deployableContainer)); + Assert.assertTrue("afterStop() should have been called", listener.afterStop); + } + + @Test + public void shouldNotifyListenerOnKillEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + fire(new BeforeKill(deployableContainer)); + Assert.assertTrue("beforeKill() should have been called", listener.beforeKill); + + fire(new AfterKill(deployableContainer)); + Assert.assertTrue("afterKill() should have been called", listener.afterKill); + } + + @Test + public void shouldNotifyListenerOnDeployEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + DeploymentDescription description = + new DeploymentDescription("test", ShrinkWrap.create(JavaArchive.class)); + + fire(new BeforeDeploy(deployableContainer, description)); + Assert.assertTrue("beforeDeploy() should have been called", listener.beforeDeploy); + Assert.assertSame(description, listener.deploymentDescription); + + fire(new AfterDeploy(deployableContainer, description)); + Assert.assertTrue("afterDeploy() should have been called", listener.afterDeploy); + } + + @Test + public void shouldNotifyListenerOnUndeployEvents() throws Exception { + TrackingContainerLifecycleListener listener = new TrackingContainerLifecycleListener(); + getManager().addListener(ContainerLifecycleListener.class, listener); + + DeploymentDescription description = + new DeploymentDescription("test", ShrinkWrap.create(JavaArchive.class)); + + fire(new BeforeUnDeploy(deployableContainer, description)); + Assert.assertTrue("beforeUndeploy() should have been called", listener.beforeUndeploy); + + fire(new AfterUnDeploy(deployableContainer, description)); + Assert.assertTrue("afterUndeploy() should have been called", listener.afterUndeploy); + } + + private static class TrackingContainerLifecycleListener implements ContainerLifecycleListener { + boolean beforeSetup = false; + boolean afterSetup = false; + boolean beforeStart = false; + boolean afterStart = false; + boolean beforeStop = false; + boolean afterStop = false; + boolean beforeKill = false; + boolean afterKill = false; + boolean beforeDeploy = false; + boolean afterDeploy = false; + boolean beforeUndeploy = false; + boolean afterUndeploy = false; + DeployableContainer deployableContainer = null; + DeploymentDescription deploymentDescription = null; + + @Override + public void beforeSetup(DeployableContainer dc) throws Exception { + beforeSetup = true; + deployableContainer = dc; + } + + @Override + public void afterSetup(DeployableContainer dc) throws Exception { + afterSetup = true; + } + + @Override + public void beforeStart(DeployableContainer dc) throws Exception { + beforeStart = true; + } + + @Override + public void afterStart(DeployableContainer dc) throws Exception { + afterStart = true; + } + + @Override + public void beforeStop(DeployableContainer dc) throws Exception { + beforeStop = true; + } + + @Override + public void afterStop(DeployableContainer dc) throws Exception { + afterStop = true; + } + + @Override + public void beforeKill(DeployableContainer dc) throws Exception { + beforeKill = true; + } + + @Override + public void afterKill(DeployableContainer dc) throws Exception { + afterKill = true; + } + + @Override + public void beforeDeploy(DeployableContainer dc, DeploymentDescription description) throws Exception { + beforeDeploy = true; + deploymentDescription = description; + } + + @Override + public void afterDeploy(DeployableContainer dc, DeploymentDescription description) throws Exception { + afterDeploy = true; + } + + @Override + public void beforeUndeploy(DeployableContainer dc, DeploymentDescription description) throws Exception { + beforeUndeploy = true; + } + + @Override + public void afterUndeploy(DeployableContainer dc, DeploymentDescription description) throws Exception { + afterUndeploy = true; + } + } +} diff --git a/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerTestCase.java b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerTestCase.java new file mode 100644 index 000000000..ece9795a0 --- /dev/null +++ b/container/impl-base/src/test/java/org/jboss/arquillian/container/impl/ContainerMultiControlListenerTestCase.java @@ -0,0 +1,169 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.impl; + +import java.util.List; +import org.jboss.arquillian.container.spi.ContainerMultiControlListener; +import org.jboss.arquillian.container.spi.event.DeployManagedDeployments; +import org.jboss.arquillian.container.spi.event.SetupContainers; +import org.jboss.arquillian.container.spi.event.StartClassContainers; +import org.jboss.arquillian.container.spi.event.StartSuiteContainers; +import org.jboss.arquillian.container.spi.event.StopClassContainers; +import org.jboss.arquillian.container.spi.event.StopManualContainers; +import org.jboss.arquillian.container.spi.event.StopSuiteContainers; +import org.jboss.arquillian.container.spi.event.UnDeployManagedDeployments; +import org.jboss.arquillian.container.test.AbstractContainerTestBase; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that {@link ContainerMultiControlListener} instances registered via + * {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)} are notified + * for multi-container control events. + */ +public class ContainerMultiControlListenerTestCase extends AbstractContainerTestBase { + + @Override + protected void addExtensions(List> extensions) { + extensions.add(ContainerMultiControlListenerAdaptor.class); + } + + @Test + public void shouldNotifyListenerOnSetupContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new SetupContainers()); + Assert.assertTrue("setupContainers() should have been called", listener.setupContainers); + } + + @Test + public void shouldNotifyListenerOnStartSuiteContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new StartSuiteContainers()); + Assert.assertTrue("startSuiteContainers() should have been called", listener.startSuiteContainers); + } + + @Test + public void shouldNotifyListenerOnStartClassContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new StartClassContainers()); + Assert.assertTrue("startClassContainers() should have been called", listener.startClassContainers); + } + + @Test + public void shouldNotifyListenerOnStopSuiteContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new StopSuiteContainers()); + Assert.assertTrue("stopSuiteContainers() should have been called", listener.stopSuiteContainers); + } + + @Test + public void shouldNotifyListenerOnStopClassContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new StopClassContainers()); + Assert.assertTrue("stopClassContainers() should have been called", listener.stopClassContainers); + } + + @Test + public void shouldNotifyListenerOnStopManualContainers() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new StopManualContainers()); + Assert.assertTrue("stopManualContainers() should have been called", listener.stopManualContainers); + } + + @Test + public void shouldNotifyListenerOnDeployManagedDeployments() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new DeployManagedDeployments()); + Assert.assertTrue("deployManagedDeployments() should have been called", listener.deployManagedDeployments); + } + + @Test + public void shouldNotifyListenerOnUndeployManagedDeployments() throws Exception { + TrackingContainerMultiControlListener listener = new TrackingContainerMultiControlListener(); + getManager().addListener(ContainerMultiControlListener.class, listener); + + fire(new UnDeployManagedDeployments()); + Assert.assertTrue("undeployManagedDeployments() should have been called", + listener.undeployManagedDeployments); + } + + private static class TrackingContainerMultiControlListener implements ContainerMultiControlListener { + boolean setupContainers = false; + boolean startSuiteContainers = false; + boolean startClassContainers = false; + boolean stopSuiteContainers = false; + boolean stopClassContainers = false; + boolean stopManualContainers = false; + boolean deployManagedDeployments = false; + boolean undeployManagedDeployments = false; + + @Override + public void setupContainers() throws Exception { + setupContainers = true; + } + + @Override + public void startSuiteContainers() throws Exception { + startSuiteContainers = true; + } + + @Override + public void startClassContainers() throws Exception { + startClassContainers = true; + } + + @Override + public void stopSuiteContainers() throws Exception { + stopSuiteContainers = true; + } + + @Override + public void stopClassContainers() throws Exception { + stopClassContainers = true; + } + + @Override + public void stopManualContainers() throws Exception { + stopManualContainers = true; + } + + @Override + public void deployManagedDeployments() throws Exception { + deployManagedDeployments = true; + } + + @Override + public void undeployManagedDeployments() throws Exception { + undeployManagedDeployments = true; + } + } +} diff --git a/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerControlListener.java b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerControlListener.java new file mode 100644 index 000000000..fde054f04 --- /dev/null +++ b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerControlListener.java @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.spi; + +import org.jboss.arquillian.container.spi.client.deployment.Deployment; + +/** + * Listener SPI for single-container control events + * ({@code org.jboss.arquillian.container.spi.event}: SetupContainer, StartContainer, + * StopContainer, KillContainer, DeployDeployment, UnDeployDeployment). + * + *

Register instances via {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface ContainerControlListener { + + default void setupContainer(Container container) throws Exception { + } + + default void startContainer(Container container) throws Exception { + } + + default void stopContainer(Container container) throws Exception { + } + + default void killContainer(Container container) throws Exception { + } + + default void deployDeployment(Container container, Deployment deployment) throws Exception { + } + + default void undeployDeployment(Container container, Deployment deployment) throws Exception { + } +} diff --git a/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerLifecycleListener.java b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerLifecycleListener.java new file mode 100644 index 000000000..7ca47fd33 --- /dev/null +++ b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerLifecycleListener.java @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.spi; + +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; + +/** + * Listener SPI for per-container notification events + * ({@code org.jboss.arquillian.container.spi.event.container}). + * + *

Register instances via {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface ContainerLifecycleListener { + + default void beforeSetup(DeployableContainer deployableContainer) throws Exception { + } + + default void afterSetup(DeployableContainer deployableContainer) throws Exception { + } + + default void beforeStart(DeployableContainer deployableContainer) throws Exception { + } + + default void afterStart(DeployableContainer deployableContainer) throws Exception { + } + + default void beforeStop(DeployableContainer deployableContainer) throws Exception { + } + + default void afterStop(DeployableContainer deployableContainer) throws Exception { + } + + default void beforeKill(DeployableContainer deployableContainer) throws Exception { + } + + default void afterKill(DeployableContainer deployableContainer) throws Exception { + } + + default void beforeDeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + throws Exception { + } + + default void afterDeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + throws Exception { + } + + default void beforeUndeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + throws Exception { + } + + default void afterUndeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + throws Exception { + } +} diff --git a/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerMultiControlListener.java b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerMultiControlListener.java new file mode 100644 index 000000000..9ff863536 --- /dev/null +++ b/container/spi/src/main/java/org/jboss/arquillian/container/spi/ContainerMultiControlListener.java @@ -0,0 +1,56 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.container.spi; + +/** + * Listener SPI for multi-container control events + * ({@code org.jboss.arquillian.container.spi.event}: SetupContainers, StartSuiteContainers, + * StartClassContainers, StopSuiteContainers, StopClassContainers, StopManualContainers, + * DeployManagedDeployments, UnDeployManagedDeployments). + * + *

Register instances via {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface ContainerMultiControlListener { + + default void setupContainers() throws Exception { + } + + default void startSuiteContainers() throws Exception { + } + + default void startClassContainers() throws Exception { + } + + default void stopSuiteContainers() throws Exception { + } + + default void stopClassContainers() throws Exception { + } + + default void stopManualContainers() throws Exception { + } + + default void deployManagedDeployments() throws Exception { + } + + default void undeployManagedDeployments() throws Exception { + } +} diff --git a/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/CoreExtension.java b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/CoreExtension.java new file mode 100644 index 000000000..4f165b406 --- /dev/null +++ b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/CoreExtension.java @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.core.impl; + +import org.jboss.arquillian.core.spi.LoadableExtension; + +/** + * CoreExtension + * + * @author Arquillian + */ +public class CoreExtension implements LoadableExtension { + @Override + public void register(ExtensionBuilder builder) { + builder.observer(ManagerLifecycleListenerAdaptor.class); + } +} diff --git a/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerImpl.java b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerImpl.java index 10aae8148..f5d812c9a 100644 --- a/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerImpl.java +++ b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerImpl.java @@ -22,8 +22,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import org.jboss.arquillian.core.api.Injector; import org.jboss.arquillian.core.api.annotation.ApplicationScoped; import org.jboss.arquillian.core.api.event.ManagerStarted; @@ -59,6 +61,7 @@ public class ManagerImpl implements Manager { private final RuntimeLogger runtimeLogger; private final List contexts; private final List extensions; + private final Map, List> listenerRegistry = new ConcurrentHashMap, List>(); /* * Hack: * Events can be fired nested. If a nested handler throws a exception, the exception is fired on the bus for handling. @@ -329,6 +332,27 @@ public void addExtension(Class extensionClass) throws Exception { extensions.add(newExtension); } + @Override + public void addListener(Class listenerType, T listener) { + Validate.notNull(listenerType, "Listener type must be specified"); + Validate.notNull(listener, "Listener must be specified"); + if (!listenerRegistry.containsKey(listenerType)) { + listenerRegistry.putIfAbsent(listenerType, Collections.synchronizedList(new ArrayList())); + } + listenerRegistry.get(listenerType).add(listener); + } + + @Override + @SuppressWarnings("unchecked") + public List getListeners(Class listenerType) { + Validate.notNull(listenerType, "Listener type must be specified"); + List listeners = listenerRegistry.get(listenerType); + if (listeners == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList((List) listeners); + } + public void removeExtension(Class extensionClass) { for (Extension extension : extensions) { Object target = ((ExtensionImpl) extension).getTarget(); @@ -410,6 +434,8 @@ private void createBuiltInServices() throws Exception { executeInApplicationContext(new Callable() { @Override public Object call() throws Exception { + ManagerImpl.this.bind( + ApplicationScoped.class, Manager.class, ManagerImpl.this); ManagerImpl.this.bind( ApplicationScoped.class, Injector.class, InjectorImpl.of(ManagerImpl.this)); ManagerImpl.this.bind( diff --git a/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerAdaptor.java b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerAdaptor.java new file mode 100644 index 000000000..7ce13d3ee --- /dev/null +++ b/core/impl-base/src/main/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerAdaptor.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.core.impl; + +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.api.event.ManagerStarted; +import org.jboss.arquillian.core.api.event.ManagerStopping; +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.core.spi.ManagerLifecycleListener; + +/** + * Adaptor that bridges {@link ManagerStarted} and {@link ManagerStopping} events + * to the {@link ManagerLifecycleListener} SPI. + * + * @author Arquillian + */ +public class ManagerLifecycleListenerAdaptor { + + @Inject + private Instance manager; + + public void onManagerStarted(@Observes ManagerStarted event) throws Exception { + for (ManagerLifecycleListener listener : manager.get().getListeners(ManagerLifecycleListener.class)) { + listener.managerStarted(); + } + } + + public void onManagerStopping(@Observes ManagerStopping event) throws Exception { + for (ManagerLifecycleListener listener : manager.get().getListeners(ManagerLifecycleListener.class)) { + listener.managerStopping(); + } + } +} diff --git a/core/impl-base/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/core/impl-base/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 000000000..75b496d23 --- /dev/null +++ b/core/impl-base/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +org.jboss.arquillian.core.impl.CoreExtension diff --git a/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/CustomEventListenerBehaviorTestCase.java b/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/CustomEventListenerBehaviorTestCase.java new file mode 100644 index 000000000..13c5557d6 --- /dev/null +++ b/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/CustomEventListenerBehaviorTestCase.java @@ -0,0 +1,213 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.core.impl; + +import java.util.ArrayList; +import java.util.List; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.ManagerLifecycleListener; +import org.jboss.arquillian.core.spi.event.Event; +import org.jboss.arquillian.core.test.AbstractManagerTestBase; +import org.junit.Assert; +import org.junit.Test; + +/** + * Documents the behavior of the listener SPI when custom + * {@link org.jboss.arquillian.core.spi.event.Event} types are fired via + * {@link org.jboss.arquillian.core.spi.Manager#fire(Object)}. + * + *

Findings

+ *
    + *
  1. Custom events fired via {@code Manager#fire()} continue to reach + * {@code @Observes}-annotated observer methods exactly as before — + * the new listener SPI does not break the legacy event model.
  2. + *
  3. Custom events do not trigger any of the typed listener SPI + * callbacks ({@link ManagerLifecycleListener}, etc.), because the adaptor + * classes observe only the specific known event types.
  4. + *
  5. A custom event that extends a known event type (e.g. + * {@code class BeforeRules extends BeforeTestLifecycleEvent}) will fire + * all {@code @Observes} observers compatible with that type hierarchy, + * but will still not trigger any listener SPI callback, because + * the adaptor observes the concrete known subtype (e.g. {@code Before}), + * not the abstract supertype ({@code BeforeTestLifecycleEvent}).
  6. + *
  7. Consequence: framework-specific custom events (JUnit {@code BeforeRules}, + * JUnit 5 {@code RunModeEvent}, etc.) are invisible to the listener SPI. + * They can only be observed via {@code @Observes} in traditional observer + * classes. A future general-purpose listener hook point may be needed to + * cover these cases.
  8. + *
+ */ +public class CustomEventListenerBehaviorTestCase extends AbstractManagerTestBase { + + // ------------------------------------------------------------------------- + // Custom event types -- simulating what external code does + // ------------------------------------------------------------------------- + + /** A completely custom event unrelated to any standard hierarchy. */ + static class CustomSimpleEvent implements Event { + } + + /** + * A custom event that, like JUnit 4's BeforeRules, extends a standard + * lifecycle event base class but is its own concrete type. + * In real code this would extend BeforeTestLifecycleEvent; here we extend + * the common base just to demonstrate the type-hierarchy behaviour. + */ + static class CustomLifecycleEvent implements Event { + private final String phase; + + CustomLifecycleEvent(String phase) { + this.phase = phase; + } + + String getPhase() { + return phase; + } + } + + // ------------------------------------------------------------------------- + // Observer class — the traditional @Observes mechanism + // ------------------------------------------------------------------------- + + static class CustomEventObserver { + final List receivedSimple = new ArrayList(); + final List receivedLifecycle = new ArrayList(); + + /** Receives any CustomSimpleEvent. */ + public void onSimple(@Observes CustomSimpleEvent event) { + receivedSimple.add(event); + } + + /** Receives any CustomLifecycleEvent. */ + public void onLifecycle(@Observes CustomLifecycleEvent event) { + receivedLifecycle.add(event); + } + } + + // ------------------------------------------------------------------------- + // Tests + // ------------------------------------------------------------------------- + + @Override + protected void addExtensions(List> extensions) { + extensions.add(CustomEventObserver.class); + extensions.add(ManagerLifecycleListenerAdaptor.class); + } + + /** + * FINDING 1: Custom events fired via {@code Manager#fire()} still reach + * {@code @Observes}-annotated observers — the new listener SPI does not + * break the existing mechanism. + */ + @Test + public void customEventReachesLegacyObserver() { + CustomEventObserver observer = getManager().getExtension(CustomEventObserver.class); + + fire(new CustomSimpleEvent()); + Assert.assertEquals( + "Custom event should reach @Observes observer exactly once", + 1, observer.receivedSimple.size()); + + fire(new CustomLifecycleEvent("setup")); + Assert.assertEquals( + "Custom lifecycle event should reach @Observes observer exactly once", + 1, observer.receivedLifecycle.size()); + Assert.assertEquals("setup", + ((CustomLifecycleEvent) observer.receivedLifecycle.get(0)).getPhase()); + } + + /** + * FINDING 2: Custom events do NOT trigger the typed listener SPI callbacks, + * because the adaptor observes only specific known event types. + *

+ * {@link ManagerLifecycleListenerAdaptor} observes {@code ManagerStarted} + * and {@code ManagerStopping}. Firing a {@code CustomSimpleEvent} does + * not touch {@link ManagerLifecycleListener} implementations at all. + */ + @Test + public void customEventDoesNotTriggerListenerSpi() { + TrackingManagerLifecycleListener listener = new TrackingManagerLifecycleListener(); + getManager().addListener(ManagerLifecycleListener.class, listener); + + // Fire a custom event — the ManagerLifecycleListener should NOT be called + fire(new CustomSimpleEvent()); + Assert.assertFalse( + "managerStarted() must NOT be called when a custom event is fired", + listener.started); + Assert.assertFalse( + "managerStopping() must NOT be called when a custom event is fired", + listener.stopping); + } + + /** + * FINDING 3: Multiple firings of a custom event each reach the observer + * independently, verifying there is no state leak between firings. + */ + @Test + public void multipleCustomEventFiringsEachReachObserver() { + CustomEventObserver observer = getManager().getExtension(CustomEventObserver.class); + + fire(new CustomLifecycleEvent("before")); + fire(new CustomLifecycleEvent("after")); + + Assert.assertEquals( + "Both custom lifecycle events should reach the observer", + 2, observer.receivedLifecycle.size()); + Assert.assertEquals("before", + ((CustomLifecycleEvent) observer.receivedLifecycle.get(0)).getPhase()); + Assert.assertEquals("after", + ((CustomLifecycleEvent) observer.receivedLifecycle.get(1)).getPhase()); + } + + /** + * FINDING 4: The listener registry is independent per listener type. + * Registering a {@link ManagerLifecycleListener} does not affect observation + * of custom events, and vice-versa. Both coexist without interference. + */ + @Test + public void legacyObserverAndListenerSpiCoexistWithoutInterference() { + CustomEventObserver observer = getManager().getExtension(CustomEventObserver.class); + TrackingManagerLifecycleListener listener = new TrackingManagerLifecycleListener(); + getManager().addListener(ManagerLifecycleListener.class, listener); + + // Custom event: reaches observer, not listener + fire(new CustomSimpleEvent()); + Assert.assertEquals(1, observer.receivedSimple.size()); + Assert.assertFalse("Listener SPI must not be triggered by custom event", + listener.started); + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + private static class TrackingManagerLifecycleListener implements ManagerLifecycleListener { + boolean started = false; + boolean stopping = false; + + @Override + public void managerStarted() throws Exception { + started = true; + } + + @Override + public void managerStopping() throws Exception { + stopping = true; + } + } +} diff --git a/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerTestCase.java b/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerTestCase.java new file mode 100644 index 000000000..75bc8e869 --- /dev/null +++ b/core/impl-base/src/test/java/org/jboss/arquillian/core/impl/ManagerLifecycleListenerTestCase.java @@ -0,0 +1,94 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.core.impl; + +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.core.spi.ManagerBuilder; +import org.jboss.arquillian.core.spi.ManagerLifecycleListener; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that {@link ManagerLifecycleListener} instances registered via + * {@link Manager#addListener(Class, Object)} are notified when the manager starts and stops. + */ +public class ManagerLifecycleListenerTestCase { + + @Test + public void shouldNotifyListenerOnManagerStarted() throws Exception { + TrackingManagerLifecycleListener listener = new TrackingManagerLifecycleListener(); + + Manager manager = ManagerBuilder.from() + .extension(ManagerLifecycleListenerAdaptor.class) + .create(); + manager.addListener(ManagerLifecycleListener.class, listener); + manager.start(); + + Assert.assertTrue("managerStarted() should have been called", listener.started); + Assert.assertFalse("managerStopping() should not yet have been called", listener.stopping); + + manager.shutdown(); + + Assert.assertTrue("managerStopping() should have been called after shutdown", listener.stopping); + } + + @Test + public void shouldNotifyMultipleListeners() throws Exception { + TrackingManagerLifecycleListener listener1 = new TrackingManagerLifecycleListener(); + TrackingManagerLifecycleListener listener2 = new TrackingManagerLifecycleListener(); + + Manager manager = ManagerBuilder.from() + .extension(ManagerLifecycleListenerAdaptor.class) + .create(); + manager.addListener(ManagerLifecycleListener.class, listener1); + manager.addListener(ManagerLifecycleListener.class, listener2); + manager.start(); + + Assert.assertTrue("listener1.managerStarted() should have been called", listener1.started); + Assert.assertTrue("listener2.managerStarted() should have been called", listener2.started); + + manager.shutdown(); + + Assert.assertTrue("listener1.managerStopping() should have been called", listener1.stopping); + Assert.assertTrue("listener2.managerStopping() should have been called", listener2.stopping); + } + + @Test + public void shouldReturnEmptyListWhenNoListenersRegistered() throws Exception { + Manager manager = ManagerBuilder.from().create(); + Assert.assertTrue( + "getListeners() should return empty list when none registered", + manager.getListeners(ManagerLifecycleListener.class).isEmpty()); + manager.shutdown(); + } + + private static class TrackingManagerLifecycleListener implements ManagerLifecycleListener { + boolean started = false; + boolean stopping = false; + + @Override + public void managerStarted() throws Exception { + started = true; + } + + @Override + public void managerStopping() throws Exception { + stopping = true; + } + } +} diff --git a/core/spi/src/main/java/org/jboss/arquillian/core/spi/Manager.java b/core/spi/src/main/java/org/jboss/arquillian/core/spi/Manager.java index dd7ce8133..cc52e0878 100644 --- a/core/spi/src/main/java/org/jboss/arquillian/core/spi/Manager.java +++ b/core/spi/src/main/java/org/jboss/arquillian/core/spi/Manager.java @@ -17,6 +17,7 @@ package org.jboss.arquillian.core.spi; import java.lang.annotation.Annotation; +import java.util.List; /** * Manager @@ -49,4 +50,9 @@ public interface Manager { void addExtension(Class extension) throws Exception; void removeExtension(Class extension) throws Exception; + + // Listener SPI + void addListener(Class listenerType, T listener); + + List getListeners(Class listenerType); } diff --git a/core/spi/src/main/java/org/jboss/arquillian/core/spi/ManagerLifecycleListener.java b/core/spi/src/main/java/org/jboss/arquillian/core/spi/ManagerLifecycleListener.java new file mode 100644 index 000000000..1fa96b1a8 --- /dev/null +++ b/core/spi/src/main/java/org/jboss/arquillian/core/spi/ManagerLifecycleListener.java @@ -0,0 +1,35 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.core.spi; + +/** + * Listener SPI for Manager lifecycle events. + * + *

Register instances via {@link Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface ManagerLifecycleListener { + + default void managerStarted() throws Exception { + } + + default void managerStopping() throws Exception { + } +} diff --git a/docs/arch-notes/ArquillianEvents.md b/docs/arch-notes/ArquillianEvents.md new file mode 100644 index 000000000..54799f1a9 --- /dev/null +++ b/docs/arch-notes/ArquillianEvents.md @@ -0,0 +1,329 @@ +# Arquillian Event System + +This document provides a comprehensive overview of the event sources and sinks in the Arquillian framework. + +## Event Hierarchy + +Arquillian uses an event-driven architecture with a hierarchical event structure: + +### Core Events +- `Event` - Base interface for all events + - `SuiteEvent` - Base for test suite lifecycle events + - `ClassEvent` - Base for test class lifecycle events + - `TestEvent` - Base for test method lifecycle events + - `TestLifecycleEvent` - Base for test method lifecycle events with executor + - `BeforeTestLifecycleEvent` - Events fired before test execution + - `AfterTestLifecycleEvent` - Events fired after test execution + - `ClassLifecycleEvent` - Base for class lifecycle events with executor + +### Container Events +- `ContainerEvent` - Base for container lifecycle events + - `DeployerEvent` - Base for deployment events +- `ContainerMultiControlEvent` - Interface for events that control multiple containers + +### Event Hierarchy Diagram + +```mermaid +classDiagram + class Event { + <> + } + + class SuiteEvent { + +SuiteEvent() + } + + class ClassEvent { + -TestClass testClass + +ClassEvent(Class testClass) + +ClassEvent(TestClass testClass) + +getTestClass() TestClass + } + + class TestEvent { + -Object testInstance + -Method testMethod + +TestEvent(Object testInstance, Method testMethod) + +getTestInstance() Object + +getTestMethod() Method + } + + class TestLifecycleEvent { + -LifecycleMethodExecutor executor + +TestLifecycleEvent(Object testInstance, Method testMethod) + +TestLifecycleEvent(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) + +getExecutor() LifecycleMethodExecutor + } + + class BeforeTestLifecycleEvent { + +BeforeTestLifecycleEvent(Object testInstance, Method testMethod) + +BeforeTestLifecycleEvent(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) + } + + class AfterTestLifecycleEvent { + +AfterTestLifecycleEvent(Object testInstance, Method testMethod) + +AfterTestLifecycleEvent(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) + } + + class ClassLifecycleEvent { + -LifecycleMethodExecutor executor + +ClassLifecycleEvent(Class testClass) + +ClassLifecycleEvent(Class testClass, LifecycleMethodExecutor executor) + +getExecutor() LifecycleMethodExecutor + } + + class ContainerEvent { + -DeployableContainer deployableContainer + +ContainerEvent(DeployableContainer deployableContainer) + +getDeployableContainer() DeployableContainer + } + + class DeployerEvent { + -DeploymentDescription deployment + +DeployerEvent(DeployableContainer deployableContainer, DeploymentDescription deployment) + +getDeployment() DeploymentDescription + } + + class ContainerMultiControlEvent { + <> + } + + class BeforeSuite { + +BeforeSuite() + } + + class AfterSuite { + +AfterSuite() + } + + class BeforeClass { + +BeforeClass(Class testClass) + +BeforeClass(Class testClass, LifecycleMethodExecutor executor) + } + + class AfterClass { + +AfterClass(Class testClass) + +AfterClass(Class testClass, LifecycleMethodExecutor executor) + } + + class Before { + +Before(Object testInstance, Method testMethod) + +Before(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) + } + + class After { + +After(Object testInstance, Method testMethod) + +After(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) + } + + class Test { + -TestMethodExecutor testMethodExecutor + +Test(TestMethodExecutor testMethodExecutor) + +getTestMethodExecutor() TestMethodExecutor + } + + class BeforeStart { + +BeforeStart(DeployableContainer deployableContainer) + } + + class AfterStart { + +AfterStart(DeployableContainer deployableContainer) + } + + class BeforeDeploy { + +BeforeDeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + } + + class AfterDeploy { + +AfterDeploy(DeployableContainer deployableContainer, DeploymentDescription deployment) + } + + Event <|-- SuiteEvent + SuiteEvent <|-- ClassEvent + ClassEvent <|-- TestEvent + TestEvent <|-- TestLifecycleEvent + TestLifecycleEvent <|-- BeforeTestLifecycleEvent + TestLifecycleEvent <|-- AfterTestLifecycleEvent + ClassEvent <|-- ClassLifecycleEvent + + Event <|-- ContainerEvent + ContainerEvent <|-- DeployerEvent + Event <|-- ContainerMultiControlEvent + + SuiteEvent <|-- BeforeSuite + SuiteEvent <|-- AfterSuite + ClassLifecycleEvent <|-- BeforeClass + ClassLifecycleEvent <|-- AfterClass + BeforeTestLifecycleEvent <|-- Before + AfterTestLifecycleEvent <|-- After + TestEvent <|-- Test + + ContainerEvent <|-- BeforeStart + ContainerEvent <|-- AfterStart + DeployerEvent <|-- BeforeDeploy + DeployerEvent <|-- AfterDeploy +``` + +## Event Sources (Classes that fire events) + +### Test Lifecycle Event Sources +- `EventTestRunnerAdaptor` - Fires suite, class, and test lifecycle events + - Fires: `BeforeSuite`, `AfterSuite`, `BeforeClass`, `AfterClass`, `Before`, `After`, `Test` + +### Container Lifecycle Event Sources +- `ContainerEventController` - Coordinates container lifecycle with test lifecycle + - Fires: `SetupContainers`, `StartSuiteContainers`, `StopSuiteContainers`, `StartClassContainers`, `StopClassContainers`, `DeployManagedDeployments`, `UnDeployManagedDeployments` + +- `ContainerImpl` - Implementation of container operations + - Fires: `BeforeSetup`, `AfterSetup`, `BeforeStart`, `AfterStart`, `BeforeStop`, `AfterStop`, `BeforeKill`, `AfterKill` + +- `ContainerLifecycleController` - Controls container lifecycle + - Fires: `SetupContainer`, `StartContainer`, `StopContainer` + +- `ContainerDeployController` - Controls deployment lifecycle + - Fires: `DeployDeployment`, `UnDeployDeployment`, `BeforeDeploy`, `AfterDeploy`, `BeforeUnDeploy`, `AfterUnDeploy` + +### Client-Side Event Sources +- `ClientContainerController` - Client-side container control + - Fires: `StartContainer`, `DeployDeployment`, `SetupContainer`, `StopContainer`, `KillContainer` + +- `ClientDeployer` - Client-side deployment control + - Fires: `DeployDeployment`, `UnDeployDeployment` + +- `ContainerRestarter` - Handles container restarts + - Fires: `StopSuiteContainers`, `StartSuiteContainers` + +### Execution Event Sources +- `ClientTestExecuter` - Executes tests on client side + - Fires: `LocalExecutionEvent`, `RemoteExecutionEvent` + +- `ContainerTestExecuter` - Executes tests in container + - Fires: `LocalExecutionEvent` + +- `RemoteTestExecuter` - Executes tests remotely + - Fires events passed from client + +## Event Sinks (Classes that observe events) + +### Test Lifecycle Event Observers +- `TestInstanceEnricher` - Enriches test instances with resources + - Observes: `Before` + - Fires: `BeforeEnrichment`, `AfterEnrichment` + +- `TestContextHandler` - Manages test context lifecycle + - Observes: `SuiteEvent`, `ClassEvent`, `TestEvent` + +### Container Lifecycle Event Observers +- `ContainerRegistryCreator` - Creates container registry + - Observes: `ArquillianDescriptor` + +- `ContainerLifecycleController` - Controls container lifecycle + - Observes: `SetupContainers`, `StartSuiteContainers`, `StartClassContainers`, `StopSuiteContainers`, `StopClassContainers`, `StopManualContainers`, `SetupContainer`, `StartContainer`, `StopContainer`, `KillContainer` + +- `ContainerDeployController` - Controls deployment lifecycle + - Observes: `DeployManagedDeployments`, `UnDeployManagedDeployments`, `DeployDeployment`, `UnDeployDeployment` + +- `DeploymentExceptionHandler` - Handles deployment exceptions + - Observes: `DeployDeployment` + +- `ArchiveDeploymentExporter` - Exports deployments for debugging + - Observes: `BeforeDeploy` + +### Client-Side Event Observers +- `ClientContainerControllerCreator` - Creates client-side container controller + - Observes: `SetupContainers` + +- `ContainerDeployerCreator` - Creates client-side deployer + - Observes: `BeforeSuite` + +- `ClientDeployerCreator` - Creates client-side deployer + - Observes: `SetupContainers` + +- `ContainerRestarter` - Handles container restarts + - Observes: `BeforeClass` + +### Execution Event Observers +- `ClientTestExecuter` - Executes tests on client side + - Observes: `Test` + +- `ContainerTestExecuter` - Executes tests in container + - Observes: `Test` + +- `LocalTestExecuter` - Executes tests locally + - Observes: `LocalExecutionEvent` + +- `RemoteTestExecuter` - Executes tests remotely + - Observes: `RemoteExecutionEvent` + +- `BeforeLifecycleEventExecuter` - Executes before lifecycle events + - Observes: `BeforeTestLifecycleEvent` + +- `AfterLifecycleEventExecuter` - Executes after lifecycle events + - Observes: `AfterTestLifecycleEvent` + +- `ClientBeforeAfterLifecycleEventExecuter` - Executes client-side lifecycle events + - Observes: `BeforeClass`, `AfterClass`, `BeforeTestLifecycleEvent`, `AfterTestLifecycleEvent` + +### Configuration Event Observers +- `ConfigurationRegistrar` - Loads and registers configuration + - Observes: `ManagerStarted` + +- `ProtocolRegistryCreator` - Creates protocol registry + - Observes: `ArquillianDescriptor` + +### Command Event Observers +- `ContainerCommandObserver` - Handles container commands + - Observes: `StartContainerCommand`, `StopContainerCommand`, `KillContainerCommand`, `ContainerStartedCommand` + +- `DeploymentCommandObserver` - Handles deployment commands + - Observes: `DeployDeploymentCommand`, `UnDeployDeploymentCommand`, `GetDeploymentCommand` + +- `RemoteResourceCommandObserver` - Handles resource commands + - Observes: `RemoteResourceCommand` + +## Event Flow + +### Test Suite Lifecycle +1. `BeforeSuite` event is fired + - `ContainerEventController` observes and fires `SetupContainers` and `StartSuiteContainers` + - `ContainerLifecycleController` observes and sets up/starts suite containers + +2. `AfterSuite` event is fired + - `ContainerEventController` observes and fires `StopSuiteContainers` + - `ContainerLifecycleController` observes and stops suite containers + +### Test Class Lifecycle +1. `BeforeClass` event is fired + - `ContainerEventController` observes and fires `StartClassContainers`, `GenerateDeployment`, and `DeployManagedDeployments` + - `ContainerLifecycleController` observes and starts class containers + - `DeploymentGenerator` observes `GenerateDeployment` and generates deployments + - `ContainerDeployController` observes `DeployManagedDeployments` and deploys managed deployments + +2. `AfterClass` event is fired + - `ContainerEventController` observes and fires `UnDeployManagedDeployments`, `StopManualContainers`, and `StopClassContainers` + - `ContainerDeployController` observes `UnDeployManagedDeployments` and undeploys managed deployments + - `ContainerLifecycleController` observes and stops class containers + +### Test Method Lifecycle +1. `Before` event is fired + - `TestInstanceEnricher` observes and enriches test instance + +2. `Test` event is fired + - `ClientTestExecuter` or `ContainerTestExecuter` observes and executes test + +3. `After` event is fired + - Cleanup operations are performed + +## Key Event Patterns + +### Observer Pattern +Arquillian uses the observer pattern extensively through the `@Observes` annotation to decouple event producers from event consumers. + +### Event Context +Many events are wrapped in an `EventContext` to allow for interception and modification of the event flow. + +### Event Hierarchy +Events are organized in a hierarchy to allow for different levels of granularity in observation. + +### Event Precedence +Observers can specify a precedence value to control the order of execution. \ No newline at end of file diff --git a/docs/arch-notes/ArquillianSpiProposal.md b/docs/arch-notes/ArquillianSpiProposal.md new file mode 100644 index 000000000..5ceb205b6 --- /dev/null +++ b/docs/arch-notes/ArquillianSpiProposal.md @@ -0,0 +1,536 @@ +# Arquillian SPI Proposal: Lifecycle Hook System + +This document proposes a Service Provider Interface (SPI) design that could replace the current event-driven architecture in Arquillian with a more direct and explicit hook-based system. + +## Current Challenges with Event-Based Architecture + +While the event-driven architecture in Arquillian provides flexibility and loose coupling, it also has some drawbacks: + +1. **Implicit Flow**: The event flow is implicit, making it difficult to understand the execution path without deep knowledge of the codebase. +2. **Debugging Complexity**: Debugging event-based systems can be challenging as the flow of control jumps between components. +3. **Performance Overhead**: Event dispatch has some performance overhead, especially with many observers. +4. **Discoverability**: It's not immediately clear which events are available to observe or which components observe them. +5. **Order Dependencies**: Managing execution order through precedence can be error-prone. + +## Proposed SPI: Lifecycle Hook System + +The proposed SPI would replace the event system with a more explicit hook-based system that maintains extensibility while providing clearer control flow. + +### Core Concepts + +1. **Lifecycle Phases**: Well-defined phases in the test execution lifecycle +2. **Hook Points**: Specific points within phases where extensions can register callbacks +3. **Hook Registry**: Central registry for managing hook registrations +4. **Priority System**: Explicit ordering of hook execution +5. **Context Objects**: Shared state passed between hooks + +### Class Diagram + +```mermaid +classDiagram + %% Core Interfaces + class HookSystem { + <> + +registerHook(HookPoint~T~ hookPoint, Hook~T~ hook, int priority) void + +executeHooks(HookPoint~T~ hookPoint, T context) void + +hasHooks(HookPoint~T~ hookPoint) boolean + } + + class HookPoint~T~ { + <> + +getId() String + +getContextType() Class~T~ + } + + class Hook~T~ { + <> + +execute(T context) void + } + + class HookContext { + <> + +getPhase() LifecyclePhase + } + + class LifecyclePhase { + <> + SUITE + CLASS + TEST + CONTAINER + DEPLOYMENT + } + + class ArquillianExtension { + <> + +register(HookSystem hookSystem) void + } + + %% Context Classes + class SuiteContext { + -LifecyclePhase phase + +getPhase() LifecyclePhase + } + + class ClassContext { + -LifecyclePhase phase + -TestClass testClass + +ClassContext(TestClass testClass) + +getPhase() LifecyclePhase + +getTestClass() TestClass + } + + class TestContext { + -LifecyclePhase phase + -Object testInstance + -Method testMethod + -TestResult testResult + +TestContext(Object testInstance, Method testMethod) + +getPhase() LifecyclePhase + +getTestInstance() Object + +getTestMethod() Method + +getTestResult() TestResult + +setTestResult(TestResult testResult) void + } + + class ContainerContext { + -LifecyclePhase phase + -DeployableContainer container + +ContainerContext(DeployableContainer container) + +getPhase() LifecyclePhase + +getContainer() DeployableContainer + } + + class DeploymentContext { + -LifecyclePhase phase + -DeployableContainer container + -DeploymentDescription deployment + +DeploymentContext(DeployableContainer container, DeploymentDescription deployment) + +getPhase() LifecyclePhase + +getContainer() DeployableContainer + +getDeployment() DeploymentDescription + } + + %% Implementation Classes + class HookPointImpl~T~ { + -String id + -Class~T~ contextType + +HookPointImpl(String id, Class~T~ contextType) + +getId() String + +getContextType() Class~T~ + } + + class ArquillianHookPoints { + <> + +BEFORE_SUITE: HookPoint~SuiteContext~ + +AFTER_SUITE: HookPoint~SuiteContext~ + +BEFORE_CLASS: HookPoint~ClassContext~ + +AFTER_CLASS: HookPoint~ClassContext~ + +BEFORE_TEST: HookPoint~TestContext~ + +TEST: HookPoint~TestContext~ + +AFTER_TEST: HookPoint~TestContext~ + +BEFORE_CONTAINER_SETUP: HookPoint~ContainerContext~ + +AFTER_CONTAINER_SETUP: HookPoint~ContainerContext~ + +BEFORE_CONTAINER_START: HookPoint~ContainerContext~ + +AFTER_CONTAINER_START: HookPoint~ContainerContext~ + +BEFORE_DEPLOYMENT: HookPoint~DeploymentContext~ + +AFTER_DEPLOYMENT: HookPoint~DeploymentContext~ + -ArquillianHookPoints() + } + + class ContainerLifecycleController { + +register(HookSystem hookSystem) void + -setupContainers(SuiteContext context) void + -startSuiteContainers(SuiteContext context) void + -stopSuiteContainers(SuiteContext context) void + -startClassContainers(ClassContext context) void + -stopClassContainers(ClassContext context) void + } + + class ArquillianTestRunner { + -HookSystem hookSystem + +ArquillianTestRunner(HookSystem hookSystem) + +runSuite() void + +runClass(TestClass testClass) void + +runTest(Object testInstance, Method testMethod) void + } + + %% Relationships + HookContext <|-- SuiteContext + HookContext <|-- ClassContext + HookContext <|-- TestContext + HookContext <|-- ContainerContext + HookContext <|-- DeploymentContext + + HookPoint <|.. HookPointImpl + ArquillianExtension <|.. ContainerLifecycleController + + HookSystem ..> HookPoint : uses + HookSystem ..> Hook : uses + HookPoint ..> HookContext : uses + Hook ..> HookContext : uses + + ArquillianHookPoints ..> HookPointImpl : creates + ArquillianHookPoints ..> SuiteContext : references + ArquillianHookPoints ..> ClassContext : references + ArquillianHookPoints ..> TestContext : references + ArquillianHookPoints ..> ContainerContext : references + ArquillianHookPoints ..> DeploymentContext : references + + ArquillianTestRunner ..> HookSystem : uses + ArquillianTestRunner ..> ArquillianHookPoints : uses + ArquillianTestRunner ..> SuiteContext : creates + ArquillianTestRunner ..> ClassContext : creates + ArquillianTestRunner ..> TestContext : creates + + ContainerLifecycleController ..> HookSystem : uses + ContainerLifecycleController ..> ArquillianHookPoints : uses + ContainerLifecycleController ..> SuiteContext : uses + ContainerLifecycleController ..> ClassContext : uses + + HookContext ..> LifecyclePhase : uses +``` + +### Key Interfaces + +```java +/** + * Main interface for the hook system + */ +public interface HookSystem { + /** + * Register a hook to be executed at a specific hook point + */ + void registerHook(HookPoint hookPoint, Hook hook, int priority); + + /** + * Execute all hooks registered for a specific hook point + */ + void executeHooks(HookPoint hookPoint, T context); + + /** + * Check if any hooks are registered for a specific hook point + */ + boolean hasHooks(HookPoint hookPoint); +} + +/** + * Represents a specific point in the lifecycle where extensions can hook in + */ +public interface HookPoint { + /** + * Get the unique identifier for this hook point + */ + String getId(); + + /** + * Get the context class for this hook point + */ + Class getContextType(); +} + +/** + * A hook that can be executed at a specific hook point + */ +public interface Hook { + /** + * Execute the hook with the provided context + */ + void execute(T context) throws Exception; +} + +/** + * Base interface for context objects passed to hooks + */ +public interface HookContext { + /** + * Get the current phase of execution + */ + LifecyclePhase getPhase(); +} +``` + +### Predefined Hook Points + +The SPI would define standard hook points that correspond to the current event system: + +```java +public final class ArquillianHookPoints { + // Suite lifecycle + public static final HookPoint BEFORE_SUITE = new HookPointImpl<>("beforeSuite", SuiteContext.class); + public static final HookPoint AFTER_SUITE = new HookPointImpl<>("afterSuite", SuiteContext.class); + + // Class lifecycle + public static final HookPoint BEFORE_CLASS = new HookPointImpl<>("beforeClass", ClassContext.class); + public static final HookPoint AFTER_CLASS = new HookPointImpl<>("afterClass", ClassContext.class); + + // Test lifecycle + public static final HookPoint BEFORE_TEST = new HookPointImpl<>("beforeTest", TestContext.class); + public static final HookPoint TEST = new HookPointImpl<>("test", TestContext.class); + public static final HookPoint AFTER_TEST = new HookPointImpl<>("afterTest", TestContext.class); + + // Container lifecycle + public static final HookPoint BEFORE_CONTAINER_SETUP = new HookPointImpl<>("beforeContainerSetup", ContainerContext.class); + public static final HookPoint AFTER_CONTAINER_SETUP = new HookPointImpl<>("afterContainerSetup", ContainerContext.class); + public static final HookPoint BEFORE_CONTAINER_START = new HookPointImpl<>("beforeContainerStart", ContainerContext.class); + public static final HookPoint AFTER_CONTAINER_START = new HookPointImpl<>("afterContainerStart", ContainerContext.class); + + // Deployment lifecycle + public static final HookPoint BEFORE_DEPLOYMENT = new HookPointImpl<>("beforeDeployment", DeploymentContext.class); + public static final HookPoint AFTER_DEPLOYMENT = new HookPointImpl<>("afterDeployment", DeploymentContext.class); + + // Private constructor to prevent instantiation + private ArquillianHookPoints() {} +} +``` + +### Context Objects + +Context objects would provide access to relevant state and functionality: + +```java +public class SuiteContext implements HookContext { + private final LifecyclePhase phase = LifecyclePhase.SUITE; + + @Override + public LifecyclePhase getPhase() { + return phase; + } +} + +public class ClassContext implements HookContext { + private final LifecyclePhase phase = LifecyclePhase.CLASS; + private final TestClass testClass; + + public ClassContext(TestClass testClass) { + this.testClass = testClass; + } + + @Override + public LifecyclePhase getPhase() { + return phase; + } + + public TestClass getTestClass() { + return testClass; + } +} + +public class TestContext implements HookContext { + private final LifecyclePhase phase = LifecyclePhase.TEST; + private final Object testInstance; + private final Method testMethod; + private TestResult testResult; + + public TestContext(Object testInstance, Method testMethod) { + this.testInstance = testInstance; + this.testMethod = testMethod; + } + + @Override + public LifecyclePhase getPhase() { + return phase; + } + + public Object getTestInstance() { + return testInstance; + } + + public Method getTestMethod() { + return testMethod; + } + + public TestResult getTestResult() { + return testResult; + } + + public void setTestResult(TestResult testResult) { + this.testResult = testResult; + } +} + +public class ContainerContext implements HookContext { + private final LifecyclePhase phase = LifecyclePhase.CONTAINER; + private final DeployableContainer container; + + public ContainerContext(DeployableContainer container) { + this.container = container; + } + + @Override + public LifecyclePhase getPhase() { + return phase; + } + + public DeployableContainer getContainer() { + return container; + } +} + +public class DeploymentContext implements HookContext { + private final LifecyclePhase phase = LifecyclePhase.DEPLOYMENT; + private final DeployableContainer container; + private final DeploymentDescription deployment; + + public DeploymentContext(DeployableContainer container, DeploymentDescription deployment) { + this.container = container; + this.deployment = deployment; + } + + @Override + public LifecyclePhase getPhase() { + return phase; + } + + public DeployableContainer getContainer() { + return container; + } + + public DeploymentDescription getDeployment() { + return deployment; + } +} +``` + +### Extension Registration + +Extensions would register hooks through a service provider mechanism: + +```java +public interface ArquillianExtension { + /** + * Register hooks with the hook system + */ + void register(HookSystem hookSystem); +} +``` + +Extensions would be discovered using the ServiceLoader mechanism: + +```java +ServiceLoader extensions = ServiceLoader.load(ArquillianExtension.class); +for (ArquillianExtension extension : extensions) { + extension.register(hookSystem); +} +``` + +### Example Usage + +Here's how the container lifecycle controller might be implemented using the hook system: + +```java +public class ContainerLifecycleController implements ArquillianExtension { + @Override + public void register(HookSystem hookSystem) { + // Register hooks for container lifecycle + hookSystem.registerHook(ArquillianHookPoints.BEFORE_SUITE, this::setupContainers, 100); + hookSystem.registerHook(ArquillianHookPoints.BEFORE_SUITE, this::startSuiteContainers, 90); + hookSystem.registerHook(ArquillianHookPoints.AFTER_SUITE, this::stopSuiteContainers, 100); + + hookSystem.registerHook(ArquillianHookPoints.BEFORE_CLASS, this::startClassContainers, 100); + hookSystem.registerHook(ArquillianHookPoints.AFTER_CLASS, this::stopClassContainers, 100); + } + + private void setupContainers(SuiteContext context) { + // Setup containers + } + + private void startSuiteContainers(SuiteContext context) { + // Start suite containers + } + + private void stopSuiteContainers(SuiteContext context) { + // Stop suite containers + } + + private void startClassContainers(ClassContext context) { + // Start class containers + } + + private void stopClassContainers(ClassContext context) { + // Stop class containers + } +} +``` + +And here's how the test runner would execute the hooks: + +```java +public class ArquillianTestRunner { + private final HookSystem hookSystem; + + public ArquillianTestRunner(HookSystem hookSystem) { + this.hookSystem = hookSystem; + } + + public void runSuite() { + SuiteContext context = new SuiteContext(); + + try { + // Execute before suite hooks + hookSystem.executeHooks(ArquillianHookPoints.BEFORE_SUITE, context); + + // Run test classes + + } finally { + // Execute after suite hooks + hookSystem.executeHooks(ArquillianHookPoints.AFTER_SUITE, context); + } + } + + public void runClass(TestClass testClass) { + ClassContext context = new ClassContext(testClass); + + try { + // Execute before class hooks + hookSystem.executeHooks(ArquillianHookPoints.BEFORE_CLASS, context); + + // Run test methods + + } finally { + // Execute after class hooks + hookSystem.executeHooks(ArquillianHookPoints.AFTER_CLASS, context); + } + } + + public void runTest(Object testInstance, Method testMethod) { + TestContext context = new TestContext(testInstance, testMethod); + + try { + // Execute before test hooks + hookSystem.executeHooks(ArquillianHookPoints.BEFORE_TEST, context); + + // Execute test hooks + hookSystem.executeHooks(ArquillianHookPoints.TEST, context); + + } finally { + // Execute after test hooks + hookSystem.executeHooks(ArquillianHookPoints.AFTER_TEST, context); + } + } +} +``` + +## Benefits of the Hook System + +1. **Explicit Flow**: The flow of execution is more explicit and easier to follow. +2. **Improved Debugging**: Debugging is simpler as the control flow is more direct. +3. **Better Performance**: Direct method calls can be more efficient than event dispatch. +4. **Enhanced Discoverability**: Predefined hook points make it clear where extensions can integrate. +5. **Explicit Ordering**: Priority system makes execution order more explicit. +6. **Type Safety**: Context objects provide type-safe access to relevant state. +7. **Simplified Extension Model**: Extensions register hooks directly rather than observing events. + +## Migration Strategy + +To migrate from the event system to the hook system: + +1. **Parallel Implementation**: Implement the hook system alongside the event system. +2. **Bridge Layer**: Create a bridge that translates events to hook calls and vice versa. +3. **Gradual Migration**: Migrate components one by one from events to hooks. +4. **Deprecation**: Deprecate the event system once all components have been migrated. +5. **Removal**: Remove the event system in a future major version. + +## Conclusion + +The proposed hook-based SPI would provide a more explicit and direct way for extensions to integrate with Arquillian while maintaining the flexibility and extensibility of the current event-based system. It would improve code clarity, debugging, and performance while providing a clearer extension model. \ No newline at end of file diff --git a/docs/arch-notes/ArqullianDescriptor.md b/docs/arch-notes/ArqullianDescriptor.md new file mode 100644 index 000000000..96019dfe8 --- /dev/null +++ b/docs/arch-notes/ArqullianDescriptor.md @@ -0,0 +1,27 @@ +# Main Uses of ArquillianDescriptor in Arquillian + +The ArquillianDescriptor interface is a central component in the Arquillian framework that serves as the programmatic representation of the Arquillian configuration. It has several key uses throughout the project: + +## 1. Configuration Management +- **Configuration Representation**: ArquillianDescriptor is the object model for the `arquillian.xml` configuration file, providing a type-safe way to access and manipulate configuration settings. +- **Configuration Loading**: The ConfigurationRegistrar loads the configuration from `arquillian.xml` and creates an ArquillianDescriptor instance that's made available throughout the application. +- **Property Resolution**: The framework supports placeholder resolution in configuration values, allowing for system properties, environment variables, and other sources to be used in the configuration. + +## 2. Container Configuration +- **Container Registry Creation**: The ContainerRegistryCreator observes the ArquillianDescriptor and uses it to create and configure containers based on the defined container configurations. +- **Container Selection**: ArquillianDescriptor is used to determine which containers should be activated based on default settings or explicit activation through command-line properties. +- **Container Validation**: The framework validates container configurations to ensure only one container or group is marked as default. + +## 3. Deployment Configuration +- **Deployment Export Settings**: The ArchiveDeploymentExporter uses ArquillianDescriptor to determine if and how deployments should be exported to the filesystem for debugging purposes. +- **Engine Configuration**: The EngineDef part of ArquillianDescriptor controls framework-wide settings like deployment export paths and container restart behavior. + +## 4. Extension Configuration +- **Extension Management**: ArquillianDescriptor provides access to extension configurations, allowing extensions to be configured through the same configuration mechanism. +- **Protocol Configuration**: The DefaultProtocolDef part of ArquillianDescriptor defines the default communication protocol used between test client and container. + +## 5. Hierarchical Configuration Structure +- **Nested Configuration**: The interface defines a hierarchical structure with container, group, extension, and protocol configurations. +- **Fluent API**: ArquillianDescriptor and its related interfaces provide a fluent API for configuration, making it easy to create and modify configurations programmatically. + +The ArquillianDescriptor is a core part of Arquillian's extensible architecture, providing a consistent way to configure all aspects of the testing framework while supporting multiple configuration sources and formats. \ No newline at end of file diff --git a/docs/arch-notes/TestFrameworkIntegration.md b/docs/arch-notes/TestFrameworkIntegration.md new file mode 100644 index 000000000..a5136abd8 --- /dev/null +++ b/docs/arch-notes/TestFrameworkIntegration.md @@ -0,0 +1,516 @@ +# Test Framework Integration — Class Diagrams + +This document shows how JUnit 4, JUnit 5 (Jupiter), and TestNG each integrate with Arquillian +using Mermaid class diagrams. All three frameworks share a common adaptor layer +(`TestRunnerAdaptor` / `EventTestRunnerAdaptor`) and differ only in the entry-point glue code. + +--- + +## 1. Shared Infrastructure + +All three frameworks delegate to the same `EventTestRunnerAdaptor`, which translates lifecycle +calls into `Manager#fire()` events. The `Manager` event bus then dispatches to all registered +`@Observes` observers. + +```mermaid +classDiagram + class TestRunnerAdaptor { + <> + +beforeSuite() void + +afterSuite() void + +beforeClass(Class, LifecycleMethodExecutor) void + +afterClass(Class, LifecycleMethodExecutor) void + +before(Object, Method, LifecycleMethodExecutor) void + +after(Object, Method, LifecycleMethodExecutor) void + +test(TestMethodExecutor) TestResult + +fireCustomLifecycle(TestLifecycleEvent) void + +shutdown() void + } + + class EventTestRunnerAdaptor { + -manager Manager + +beforeSuite() void + +afterSuite() void + +beforeClass(Class, LifecycleMethodExecutor) void + +afterClass(Class, LifecycleMethodExecutor) void + +before(Object, Method, LifecycleMethodExecutor) void + +after(Object, Method, LifecycleMethodExecutor) void + +test(TestMethodExecutor) TestResult + +fireCustomLifecycle(TestLifecycleEvent) void + +shutdown() void + } + + class TestRunnerAdaptorBuilder { + +build(TestMethodExecutor) TestRunnerAdaptor$ + } + + class TestMethodExecutor { + <> + +invoke() void + +getInstance() Object + +getMethod() Method + +getTestClass() Class + } + + class Manager { + <> + +fire(Object) void + +addListener(Class~T~, T) void + +getListeners(Class~T~) List~T~ + +getHookSystem() LifecycleHookSystem + } + + class ClientTestExecuter { + +execute(Test) void + } + + TestRunnerAdaptor <|.. EventTestRunnerAdaptor : implements + TestRunnerAdaptorBuilder ..> TestRunnerAdaptor : creates + EventTestRunnerAdaptor --> Manager : fires events via fire() + EventTestRunnerAdaptor ..> TestMethodExecutor : wraps in Test event + Manager ..> ClientTestExecuter : notifies via @Observes Test +``` + +**Key point:** `TestRunnerAdaptorBuilder` uses `LoadableExtensionLoader` to discover and load +all `LoadableExtension` SPI implementations before returning the configured adaptor. + +--- + +## 2. JUnit 4 Integration + +### 2a. Runner and Lifecycle Delegation + +```mermaid +classDiagram + class BlockJUnit4ClassRunner { + <> + +run(RunNotifier) void + +runChild(FrameworkMethod, RunNotifier) void + } + + class Arquillian { + -adaptorManager AdaptorManager + +run(RunNotifier) void + +createTest() Object + +withBefores(FrameworkMethod, Object, Statement) Statement + +withAfters(FrameworkMethod, Object, Statement) Statement + } + + class AdaptorManager { + <> + #adaptor TestRunnerAdaptor + +run(RunNotifier)$ void + +fireCustomLifecycleEvent(TestLifecycleEvent)$ void + +getAdaptor() TestRunnerAdaptor + } + + class AdaptorManagerWithNotifier { + -notifier RunNotifier + +run(RunNotifier) void + +fireCustomLifecycleEvent(TestLifecycleEvent) void + } + + class MethodInvoker { + <> + +evaluate() void + +evaluateLifecycle()$ void + } + + class ArquillianTestClass { + <> + +apply(Statement, Description) Statement + } + + class ArquillianTest { + <> + +apply(Statement, FrameworkMethod, Object) Statement + } + + class RulesEnricher { + +enrich(RulesEnrichment, Object) void + } + + class JUnitCoreExtension { + <> + +register(ExtensionBuilder) void + } + + class JUnitTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + BlockJUnit4ClassRunner <|-- Arquillian : extends + AdaptorManager <|-- AdaptorManagerWithNotifier : extends + Arquillian --> AdaptorManager : delegates lifecycle to + AdaptorManagerWithNotifier --> ArquillianTestClass : creates as class-level rule + AdaptorManagerWithNotifier --> ArquillianTest : creates as method-level rule + ArquillianTestClass --> MethodInvoker : wraps as JUnit Statement + ArquillianTest --> MethodInvoker : wraps as JUnit Statement + MethodInvoker ..> RulesEnricher : fires RulesEnrichment event + JUnitCoreExtension ..> JUnitTestRunner : registers as observer + JUnitCoreExtension ..> RulesEnricher : registers as observer +``` + +### 2b. JUnit 4 Custom Lifecycle Events + +JUnit 4 fires several custom event types that extend the standard lifecycle hierarchy. These +events are dispatched via `Manager#fire()` and reach `@Observes` observers, but are **not** +visible to the `TestLifecycleListener` SPI (which observes only exact concrete types like +`Before`/`After`). + +```mermaid +classDiagram + class TestLifecycleEvent { + <> + +getTestInstance() Object + +getTestMethod() Method + +getExecutor() LifecycleMethodExecutor + } + + class BeforeTestLifecycleEvent { + <> + } + + class AfterTestLifecycleEvent { + <> + } + + class Before { + } + + class After { + } + + class BeforeRules { + -statement Statement + -testClass TestClass + } + + class AfterRules { + -statement Statement + -testClass TestClass + } + + class RulesEnrichment { + -statement Statement + -testClass TestClass + } + + TestLifecycleEvent <|-- BeforeTestLifecycleEvent : extends + TestLifecycleEvent <|-- AfterTestLifecycleEvent : extends + BeforeTestLifecycleEvent <|-- Before : extends + BeforeTestLifecycleEvent <|-- BeforeRules : extends + BeforeTestLifecycleEvent <|-- RulesEnrichment : extends + AfterTestLifecycleEvent <|-- After : extends + AfterTestLifecycleEvent <|-- AfterRules : extends +``` + +--- + +## 3. JUnit 5 (Jupiter) Integration + +### 3a. Extension and Lifecycle Manager + +JUnit 5 uses the `Extension` SPI rather than a custom runner. `ArquillianExtension` is +registered via `@ExtendWith(ArquillianExtension.class)` and implements seven JUnit 5 extension +interfaces. + +```mermaid +classDiagram + class BeforeAllCallback { + <> + +beforeAll(ExtensionContext) void + } + + class AfterAllCallback { + <> + +afterAll(ExtensionContext) void + } + + class BeforeEachCallback { + <> + +beforeEach(ExtensionContext) void + } + + class AfterEachCallback { + <> + +afterEach(ExtensionContext) void + } + + class BeforeTestExecutionCallback { + <> + +beforeTestExecution(ExtensionContext) void + } + + class InvocationInterceptor { + <> + +interceptTestMethod(Invocation, ReflectiveInvocationContext, ExtensionContext) void + } + + class ParameterResolver { + <> + +supportsParameter(ParameterContext, ExtensionContext) boolean + +resolveParameter(ParameterContext, ExtensionContext) Object + } + + class ArquillianExtension { + +beforeAll(ExtensionContext) void + +afterAll(ExtensionContext) void + +beforeEach(ExtensionContext) void + +afterEach(ExtensionContext) void + +beforeTestExecution(ExtensionContext) void + +interceptTestMethod(Invocation, ReflectiveInvocationContext, ExtensionContext) void + +supportsParameter(ParameterContext, ExtensionContext) boolean + +resolveParameter(ParameterContext, ExtensionContext) Object + } + + class JUnitJupiterTestClassLifecycleManager { + <> + <> + -adaptor TestRunnerAdaptor + +beforeAll(Class) void + +afterAll() void + +beforeEach(Object, Method) void + +afterEach(Object, Method) void + +close() void + } + + class MethodParameterObserver { + +on(MethodParameterProducerEvent) void + } + + class RunModeEventHandler { + +on(RunModeEvent) void + } + + class JUnitJupiterCoreExtension { + <> + +register(ExtensionBuilder) void + } + + class JUnitJupiterTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + BeforeAllCallback <|.. ArquillianExtension : implements + AfterAllCallback <|.. ArquillianExtension : implements + BeforeEachCallback <|.. ArquillianExtension : implements + AfterEachCallback <|.. ArquillianExtension : implements + BeforeTestExecutionCallback <|.. ArquillianExtension : implements + InvocationInterceptor <|.. ArquillianExtension : implements + ParameterResolver <|.. ArquillianExtension : implements + + ArquillianExtension --> JUnitJupiterTestClassLifecycleManager : stores in ExtensionContext.Store + JUnitJupiterTestClassLifecycleManager --> TestRunnerAdaptor : uses + JUnitJupiterCoreExtension ..> MethodParameterObserver : registers + JUnitJupiterCoreExtension ..> RunModeEventHandler : registers + JUnitJupiterCoreExtension ..> JUnitJupiterTestRunner : registers +``` + +### 3b. JUnit 5 Custom Lifecycle Events + +JUnit 5 introduces additional events beyond the standard `Before`/`After`. Like the JUnit 4 +custom events, these reach `@Observes` observers but are invisible to the `TestLifecycleListener` +SPI. + +```mermaid +classDiagram + class TestLifecycleEvent { + <> + } + + class BeforeTestLifecycleEvent { + <> + } + + class RunModeEvent { + -runAsClient boolean + +isRunAsClient() boolean + } + + class BeforeTestExecutionEvent { + } + + class MethodParameterProducerEvent { + -parameterTypes List + +getParameterTypes() List + +addParameter(Object) void + +getParameters() List + } + + TestLifecycleEvent <|-- BeforeTestLifecycleEvent : extends + BeforeTestLifecycleEvent <|-- RunModeEvent : extends + TestLifecycleEvent <|-- BeforeTestExecutionEvent : extends + TestLifecycleEvent <|-- MethodParameterProducerEvent : extends +``` + +--- + +## 4. TestNG Integration + +TestNG uses a base class (`Arquillian`) rather than a runner or extension. Tests extend this +class, which implements `IHookable` so TestNG delegates test method invocation through it. An +`IInvokedMethodListener` (`UpdateResultListener`) is registered via `@Listeners` to capture test +outcomes. + +```mermaid +classDiagram + class IHookable { + <> + +run(IHookCallBack, ITestResult) void + } + + class IInvokedMethodListener { + <> + +beforeInvocation(IInvokedMethod, ITestResult) void + +afterInvocation(IInvokedMethod, ITestResult) void + } + + class Arquillian { + <> + <<@Listeners(UpdateResultListener)>> + #adaptor TestRunnerAdaptor + +run(IHookCallBack, ITestResult) void + } + + class UpdateResultListener { + +afterInvocation(IInvokedMethod, ITestResult) void + } + + class TestNGCoreExtension { + <> + +register(ExtensionBuilder) void + } + + class TestNGTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + IHookable <|.. Arquillian : implements + IInvokedMethodListener <|.. UpdateResultListener : implements + Arquillian ..> UpdateResultListener : registers via @Listeners + Arquillian --> TestRunnerAdaptor : uses adaptor + TestNGCoreExtension ..> TestNGTestRunner : registers +``` + +--- + +## 5. Client vs. In-Container Execution + +All three framework runners eventually fire a `Test` event into the `Manager`. `ClientTestExecuter` +observes `Test` and decides whether to run locally (client-side) or remotely (in-container). + +```mermaid +classDiagram + class Test { + -executor TestMethodExecutor + +getExecutor() TestMethodExecutor + } + + class ClientTestExecuter { + +execute(Test) void + } + + class LocalExecutionEvent { + -executor TestMethodExecutor + +getExecutor() TestMethodExecutor + } + + class RemoteExecutionEvent { + -executor TestMethodExecutor + +getExecutor() TestMethodExecutor + } + + class JUnitTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + class JUnitJupiterTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + class TestNGTestRunner { + <> + +execute(LocalExecutionEvent) void + } + + class RemoteTestExecuter { + +execute(RemoteExecutionEvent) void + } + + ClientTestExecuter ..> Test : @Observes + ClientTestExecuter ..> LocalExecutionEvent : fires if @RunAsClient or not testable + ClientTestExecuter ..> RemoteExecutionEvent : fires if in-container + + LocalExecutionEvent <.. JUnitTestRunner : @Observes + LocalExecutionEvent <.. JUnitJupiterTestRunner : @Observes + LocalExecutionEvent <.. TestNGTestRunner : @Observes + RemoteExecutionEvent <.. RemoteTestExecuter : @Observes +``` + +**Decision logic in `ClientTestExecuter`:** +- If the deployment is marked `testable=false`, or the test method is annotated `@RunAsClient`, + fire `LocalExecutionEvent` (runs on the client JVM without going to the container). +- Otherwise fire `RemoteExecutionEvent`, which the protocol layer (Servlet or JMX) forwards to + the in-container side for execution. + +--- + +## 6. Complete Integration Overview + +```mermaid +classDiagram + class Arquillian_JUnit4 { + <> + JUnit 4 entry point + } + + class ArquillianExtension_JUnit5 { + <> + JUnit 5 entry point + } + + class Arquillian_TestNG { + <> + TestNG entry point + } + + class TestRunnerAdaptorBuilder { + +build(TestMethodExecutor) TestRunnerAdaptor$ + } + + class EventTestRunnerAdaptor { + <> + Shared adaptor layer + } + + class Manager { + <> + Core event bus + } + + class LoadableExtension { + <> + JUnitCoreExtension + JUnitJupiterCoreExtension + TestNGCoreExtension + ContainerExtension + TestExtension + ... + } + + Arquillian_JUnit4 --> TestRunnerAdaptorBuilder : uses to create adaptor + ArquillianExtension_JUnit5 --> TestRunnerAdaptorBuilder : uses to create adaptor + Arquillian_TestNG --> TestRunnerAdaptorBuilder : uses to create adaptor + + TestRunnerAdaptorBuilder ..> EventTestRunnerAdaptor : creates + TestRunnerAdaptorBuilder ..> LoadableExtension : discovers via SPI + + EventTestRunnerAdaptor --> Manager : fires lifecycle events + LoadableExtension ..> Manager : registers observers with +``` diff --git a/docs/arch-notes/arquillian_2_plan.md b/docs/arch-notes/arquillian_2_plan.md new file mode 100644 index 000000000..b948c073d --- /dev/null +++ b/docs/arch-notes/arquillian_2_plan.md @@ -0,0 +1,569 @@ +# Arquillian Core 2.0: Event-to-SPI Migration Plan + +## Context + +Arquillian Core uses an implicit event-driven architecture (`@Observes` + `Manager.fire()`) that makes execution flow hard to trace, debug, and discover. The goal is to replace this with explicit listener SPI interfaces and direct method calls, while upgrading the Java baseline from 8 to 21. + +Work has already begun on the `2.0.x` branch establishing the "listener SPI + adaptor bridge" pattern for 6 interfaces. This plan covers completing that migration across all remaining observer classes (~30+ across 4 modules), evolving the interceptor chain, and modernizing the DI interaction model. + +**Constraint**: The `junit/`, `junit5/`, and `testng/` modules are NOT modified. They continue to use `@Observes` against the existing event bus, which must remain operational for backward compatibility. + +--- + +## Phase 0: Java 21 Baseline Upgrade + +**Goal**: Establish Java 21 as compilation target before SPI work, enabling sealed interfaces, records, pattern matching, and enhanced switch in all new code. + +**Files to modify**: +- `pom.xml` (root) -- set `maven.compiler.release` from `8` to `21` +- `.github/workflows/` -- update CI matrix, JDK 21 minimum +- Remove `animal-sniffer-maven-plugin` if present (no longer needed) +- Upgrade `com.puppycrawl.tools:checkstyle` to 10.12+ for Java 21 syntax support +- Upgrade Mockito from 4.x to 5.x (required for Java 21 sealed classes) + +**Verification**: `./mvnw clean install` -- full build with existing test suite must pass with no behavioral changes. + +**Risk**: Downstream container adaptors may not support Java 21 yet. Acceptable for a 2.0.x major version. + +--- + +## Phase 1: Already Completed (Current State on 2.0.x) + +The following listener SPIs and their adaptor bridges are implemented and registered: + +| Listener SPI | Location | Adaptor | Registered In | +|---|---|---|---| +| `ManagerLifecycleListener` | core/spi | `ManagerLifecycleListenerAdaptor` | `CoreExtension` | +| `TestLifecycleListener` | test/spi | `TestLifecycleListenerAdaptor` | `TestExtension` | +| `TestEnrichmentListener` | test/spi | `TestEnrichmentListenerAdaptor` | `TestExtension` | +| `ContainerLifecycleListener` | container/spi | `ContainerLifecycleListenerAdaptor` | `ContainerExtension` | +| `ContainerControlListener` | container/spi | `ContainerControlListenerAdaptor` | `ContainerExtension` | +| `ContainerMultiControlListener` | container/spi | `ContainerMultiControlListenerAdaptor` | `ContainerExtension` | + +`Manager` already has `addListener(Class, T)` and `getListeners(Class)`. `ManagerImpl` stores listeners in `ConcurrentHashMap, List>`. + +--- + +## Phase 2: Configuration Listener SPI + +**Goal**: Create a listener SPI for configuration lifecycle, covering `ConfigurationRegistrar` and the `ArquillianDescriptor`-driven observers. + +**Modules changed**: `core/spi`, `config/impl-base`, `container/impl-base`, `container/test-impl-base` + +### New SPI + +**`core/spi/.../ConfigurationListener.java`**: +```java +public interface ConfigurationListener { + default void onDescriptorLoaded(ArquillianDescriptor descriptor) throws Exception {} +} +``` + +### New Adaptor + +**`config/impl-base/.../ConfigurationListenerAdaptor.java`**: +- `@Observes ArquillianDescriptor event` -- iterates `manager.getListeners(ConfigurationListener.class)` and calls `onDescriptorLoaded(descriptor)` +- Registered in `ConfigExtension` + +### Observer Migrations + +| Observer | Current Event | Migration | +|---|---|---| +| `ContainerRegistryCreator` | `@Observes ArquillianDescriptor` | Implement `ConfigurationListener.onDescriptorLoaded()` | +| `ProtocolRegistryCreator` | `@Observes ArquillianDescriptor` | Implement `ConfigurationListener.onDescriptorLoaded()` | + +**Challenge**: Both observers use `@Inject InstanceProducer` to bind results into scoped contexts. During this phase, keep the `@Observes` method as-is and additionally implement the listener interface as a parallel path. The adaptor calls listeners; the old observer path remains for the binding logic until Phase 6 addresses DI evolution. + +**Testing**: Existing `ContainerRegistryCreatorTestCase` must pass. Add tests verifying listener dispatch. + +--- + +## Phase 3: Container Orchestration -- Reuse Existing SPIs + +**Goal**: Migrate internal container controller observers to use the listener SPIs already created in Phase 1. + +**Modules changed**: `container/impl-base` + +### Observer Migrations + +| Observer | Current Events | Migration Target | +|---|---|---| +| `ContainerLifecycleController` (multi-container fan-out) | `@Observes SetupContainers/StartSuiteContainers/...` | Implement `ContainerMultiControlListener` | +| `ContainerLifecycleController` (per-container execution) | `@Observes SetupContainer/StartContainer/...` | Implement `ContainerControlListener` | +| `ArchiveDeploymentExporter` | `@Observes BeforeDeploy` | Implement `ContainerLifecycleListener.beforeDeploy()` | + +**Important distinction**: `ContainerControlListener` and `ContainerMultiControlListener` were designed as notification SPIs for external extensions. The controller classes being migrated here are the *implementors* of the actual logic. Both use the same SPI -- external extensions observe, controllers execute. Ordering is handled by registration order. + +### Deferred (Interceptor Pattern) + +These observers use `EventContext` + `proceed()` and are deferred to Phase 5: +- `ContainerDeploymentContextHandler` -- wraps `ContainerControlEvent`/`DeploymentEvent` with context activation +- `DeploymentExceptionHandler` -- wraps `DeployDeployment` for expected exception handling + +**Testing**: `ContainerLifecycleControllerTestCase` must pass. Verify that controller logic runs in correct order relative to notification listeners. + +--- + +## Phase 4: Container Test Orchestration (container/test-impl-base) + +**Goal**: Migrate the 15+ observers in `ContainerTestExtension` -- the largest observer module. Organized by responsibility group. + +**Modules changed**: `container/test-spi`, `container/test-impl-base` + +### Group A: Test-to-Container Bridge (ContainerEventController) + +`ContainerEventController` observes `BeforeSuite/AfterSuite/BeforeClass/AfterClass` and fires container multi-control events. It also uses the interceptor pattern for test-level context activation. + +**Migration**: Implement `TestLifecycleListener` for the non-interceptor methods: +- `beforeSuite()` -- fires `SetupContainers`, `StartSuiteContainers` +- `afterSuite()` -- fires `StopSuiteContainers` +- `beforeClass(TestClass, executor)` -- fires `StartClassContainers`, `GenerateDeployment`, `DeployManagedDeployments` +- `afterClass(TestClass, executor)` -- fires `UnDeployManagedDeployments`, `StopManualContainers`, `StopClassContainers` + +The interceptor methods (test-level context activation) are deferred to Phase 5. + +**Challenge**: The listener methods still need to fire events (`container.fire(new SetupContainers())`). The listener receives no Manager reference by default. Options: +1. Pass Manager as constructor parameter when registering +2. Have the adaptor pass Manager as an additional parameter +3. Have ContainerEventController hold a Manager reference set during initialization + +**Decision**: Use option 1 -- the `ContainerTestExtension` registration code creates the listener with the Manager reference. This is consistent with Java 21 idioms (explicit dependencies). + +### Group B: Creator/Producer Observers + +| Observer | Current Event | Migration | +|---|---|---| +| `ClientContainerControllerCreator` | `@Observes SetupContainers` | Implement `ContainerMultiControlListener.setupContainers()` | +| `ClientDeployerCreator` | `@Observes SetupContainers` | Implement `ContainerMultiControlListener.setupContainers()` | +| `ContainerContainerControllerCreator` | `@Observes BeforeSuite` | Implement `TestLifecycleListener.beforeSuite()` | +| `ContainerRestarter` | `@Observes BeforeClass` | Implement `TestLifecycleListener.beforeClass()` | + +**Challenge**: These use `@Inject @ApplicationScoped InstanceProducer` to bind values. During transition, they keep the binding via Manager.bind() called directly. Full DI extraction in Phase 6. + +### Group C: Test Execution Chain + +**New SPI: `container/test-spi/.../TestExecutionListener.java`**: +```java +public interface TestExecutionListener { + default void executeTest(TestMethodExecutor executor) throws Exception {} + default void executeLocal(TestMethodExecutor executor) throws Exception {} + default void executeRemote(TestMethodExecutor executor) throws Exception {} +} +``` + +**New Adaptor: `container/test-impl-base/.../TestExecutionListenerAdaptor.java`**: +- `@Observes Test event` -- calls `executeTest()` +- `@Observes LocalExecutionEvent event` -- calls `executeLocal()` +- `@Observes RemoteExecutionEvent event` -- calls `executeRemote()` + +| Observer | Migration | +|---|---| +| `ClientTestExecuter` | Implement `TestExecutionListener.executeTest()` | +| `LocalTestExecuter` | Implement `TestExecutionListener.executeLocal()` | +| `RemoteTestExecuter` | Implement `TestExecutionListener.executeRemote()` | + +### Group D: Lifecycle Executors -- DEFERRED + +`BeforeLifecycleEventExecuter`, `AfterLifecycleEventExecuter`, `ClientBeforeAfterLifecycleEventExecuter` use precedence to define execution boundaries. They are tightly coupled to the event ordering system and the junit/testng module contract. **Leave as `@Observes` observers permanently** -- they are the boundary between Arquillian lifecycle and test framework lifecycle. + +### Group E: Command Observers -- DEFERRED to Phase 7 + +`ContainerCommandObserver`, `DeploymentCommandObserver`, `RemoteResourceCommandObserver` form an orthogonal command bus for in-container communication. Lower priority. They can be migrated to a `CommandHandler` pattern in Phase 7 or left as-is. + +### Group F: Deployment Generation -- DEFERRED to Phase 6 + +`DeploymentGenerator` is deeply coupled to the DI system (`@Inject @ClassScoped InstanceProducer`). Migrate alongside DI evolution. + +`ArchiveDeploymentToolingExporter` can implement `ContainerLifecycleListener.beforeDeploy()` -- simple, do in this phase. + +**Testing**: `ContainerEventControllerTestCase`, `ClientTestExecuterTestCase`, `DeploymentGeneratorTestCase`. Run integration tests with `-P integration-tests`. + +--- + +## Phase 5: Interceptor Chain Evolution + +**Goal**: Replace the implicit `EventContext` + `proceed()` pattern with explicit `Interceptor` registration. + +**Modules changed**: `core/spi`, `core/impl-base`, `container/impl-base`, `container/test-impl-base` + +### New SPIs + +**`core/spi/.../Interceptor.java`**: +```java +@FunctionalInterface +public interface Interceptor { + void intercept(T event, InterceptorChain chain) throws Exception; +} +``` + +**`core/spi/.../InterceptorChain.java`**: +```java +public interface InterceptorChain { + void proceed() throws Exception; + T getEvent(); +} +``` + +### Manager API Addition + +```java + void addInterceptor(Class eventType, Interceptor interceptor, int priority); +``` + +### ManagerImpl Changes + +When `fire(event)` is called: +1. Resolve both `@Observes EventContext` observers (old) AND registered `Interceptor` instances (new) +2. Sort all together by priority +3. Build unified interceptor chain +4. Execute + +This ensures old-style (`EventContext`) and new-style (`Interceptor`) coexist and interleave correctly by priority. **Critical for backward compatibility** since `junit/core` `UpdateTestResultBeforeAfter` uses `@Observes(precedence=99) EventContext`. + +### Observer Migrations + +| Observer | Current Pattern | Migration | +|---|---|---| +| `TestContextHandler` | `@Observes(precedence=100) EventContext` | `manager.addInterceptor(SuiteEvent.class, ..., 100)` etc. | +| `ContainerDeploymentContextHandler` | `@Observes EventContext` | `manager.addInterceptor(...)` | +| `DeploymentExceptionHandler` | `@Observes EventContext` | `manager.addInterceptor(DeployDeployment.class, ...)` | +| `ContainerEventController` (test-level) | `@Observes EventContext` | `manager.addInterceptor(...)` | + +**Testing**: Critical -- interceptor ordering must be preserved exactly. +- Verify interceptors called in priority order (higher number = runs first/outermost) +- Verify `proceed()` invokes next interceptor, then observers +- Verify old `@Observes EventContext` and new `Interceptor` interleave correctly +- Verify exceptions propagate correctly through chain + +--- + +## Phase 6: DI System Evolution + +**Goal**: Reduce reliance on `@Inject Instance` / `@Inject InstanceProducer` / `@Inject Event` for listener implementations by providing explicit dependency passing. + +**Modules changed**: `core/spi`, all impl modules + +### New SPIs + +**`core/spi/.../ContextBinder.java`**: +```java +public interface ContextBinder { + void bind(Class scope, Class type, T instance); + T resolve(Class type); +} +``` + +### Convention for Listener Methods + +Listener methods that need to produce values or resolve dependencies receive a `ContextBinder` parameter. The adaptor creates it from the Manager. Example: + +```java +public interface ConfigurationListener { + default void onDescriptorLoaded(ArquillianDescriptor descriptor, ContextBinder binder) + throws Exception {} +} +``` + +### Observer Migrations (Producer Observers) + +| Observer | Current DI Pattern | Migration | +|---|---|---| +| `ConfigurationRegistrar` | `@Inject @ApplicationScoped InstanceProducer` | `binder.bind(ApplicationScoped.class, ArquillianDescriptor.class, descriptor)` | +| `ContainerRegistryCreator` | `@Inject @ApplicationScoped InstanceProducer` | Same pattern | +| `ProtocolRegistryCreator` | `@Inject @ApplicationScoped InstanceProducer` | Same pattern | +| `DeploymentGenerator` | `@Inject @ClassScoped InstanceProducer` | Same pattern | +| `ClientContainerControllerCreator` | `@Inject @ApplicationScoped InstanceProducer` | Same pattern | + +**Testing**: Each migrated producer must verify values correctly bound into expected scope. Use `AbstractManagerTestBase` infrastructure. + +**Risk**: High blast radius. Each sub-step must be independently compilable and testable. + +--- + +## Phase 7: Peripheral Module Migration + +**Goal**: Migrate remaining observers in `testenrichers/`, `protocols/`, and command observers. + +### Simple Listener Migrations + +| Observer | Module | Current Event | Migration | +|---|---|---|---| +| `BeanManagerProducer` | testenrichers/cdi | `@Observes ManagerStarted` | `ManagerLifecycleListener.managerStarted()` | +| `InitialContextProducer` | testenrichers/initialcontext | `@Observes AfterClass` | `TestLifecycleListener.afterClass()` | +| `ServletContextRegistrar` | protocols/servlet | `@Observes ManagerStarted` | `ManagerLifecycleListener.managerStarted()` | + +### Interceptor Migrations + +| Observer | Module | Migration | +|---|---|---| +| `CreationalContextDestroyer` | testenrichers/cdi | `Interceptor` (from Phase 5) | + +### Command Handler Pattern (Optional) + +**`container/test-spi/.../CommandHandler.java`**: +```java +public interface CommandHandler> { + boolean canHandle(Class> commandType); + void handle(T command) throws Exception; +} +``` + +Migrate `ContainerCommandObserver`, `DeploymentCommandObserver`, `RemoteResourceCommandObserver`. This is optional -- these can remain as `@Observes` observers if the command pattern doesn't warrant full migration. + +--- + +## Phase 8: Cleanup and Final Integration + +**Goal**: Remove adaptor bridges, evolve `EventTestRunnerAdaptor`, deprecate old APIs. + +### EventTestRunnerAdaptor Evolution + +Currently calls `manager.fire(new BeforeSuite())` etc. Evolve to: + +```java +public void beforeSuite() throws Exception { + // Direct listener dispatch (new path) + for (TestLifecycleListener l : manager.getListeners(TestLifecycleListener.class)) { + l.beforeSuite(); + } + // Event dispatch preserved for junit/testng modules and third-party extensions + manager.fire(new BeforeSuite()); +} +``` + +**Important**: Remove `TestLifecycleListenerAdaptor` at this point to prevent double dispatch (it would observe the event AND the listener was already called directly). + +### Adaptor Bridge Removal + +For each listener SPI whose adaptor bridge is no longer needed (because the caller now dispatches directly to listeners): +- Remove the adaptor class +- Remove its registration from the LoadableExtension +- Keep the listener SPI interface + +### API Deprecation + +- `@Observes` -- deprecated for new code, retained for backward compatibility +- `@Inject Instance` -- deprecated for listener implementations +- `@Inject InstanceProducer` -- deprecated, replaced by `ContextBinder` +- `@Inject Event` -- deprecated, replaced by direct `Manager.fire()` or `EventDispatcher` + +### ExtensionBuilder Enhancement + +```java +interface ExtensionBuilder { + // Existing + ExtensionBuilder observer(Class handler); + ExtensionBuilder context(Class context); + ExtensionBuilder service(Class service, Class impl); + // New + ExtensionBuilder listener(Class listenerType, Class listenerImpl); + ExtensionBuilder interceptor(Class eventType, Class> interceptor, int priority); +} +``` + +--- + +## Dependency Graph + +``` +Phase 0 (Java 21) ──────────┐ + │ │ + v │ +Phase 2 (Config) │ + │ │ +Phase 3 (Container Orch) ────┤ (Phases 2-3 can run in parallel) + │ │ + v │ +Phase 4 (Container Test) ────┤ + │ │ + v │ +Phase 5 (Interceptors) ──────┤ + │ │ + v │ +Phase 6 (DI Evolution) ──────┤ + │ │ + v │ +Phase 7 (Peripherals) ───────┤ + │ │ + v │ +Phase 8 (Cleanup) ───────────┘ +``` + +--- + +## Permanently Out of Scope + +These observer classes stay as `@Observes` forever: +- **junit/** -- `UpdateTestResultBeforeAfter`, `RulesEnricher`, `AllLifecycleEventExecutor`, `LocalTestMethodExecutor` +- **junit5/** -- `MethodParameterObserver`, `RunModeEventHandler` +- **testng/** -- `UpdateTestResultBeforeAfter`, `LocalTestMethodExecutor` +- **Lifecycle executors** -- `BeforeLifecycleEventExecuter`, `AfterLifecycleEventExecuter`, `ClientBeforeAfterLifecycleEventExecuter` (define the test framework boundary) + +--- + +## Verification Strategy + +Each phase must pass before proceeding: + +1. **Unit tests**: `./mvnw test -pl ` for each changed module +2. **Module build**: `./mvnw install -pl -am` with dependencies +3. **Full build**: `./mvnw clean install` +4. **Integration tests**: `./mvnw clean install -P integration-tests` +5. **Checkstyle**: `./mvnw checkstyle:check` (with updated checkstyle for Java 21) + +--- + +## Risk Assessment + +| Risk | Impact | Mitigation | +|---|---|---| +| Event ordering changes break behavior | High | Preserve precedence values exactly; exhaustive ordering tests | +| Double dispatch during transition (listener + observer) | Medium | Adaptor bridge pattern prevents this; remove bridges only in Phase 8 | +| junit/testng modules break | High | Never modify; they interact only via TestRunnerAdaptor + @Observes | +| DI refactoring breaks scoped context lifecycle | High | Small incremental changes per Phase 6 sub-step | +| Interceptor priority mismatch old/new | High | Unified sorting in ManagerImpl; test old+new interleaving | +| Third-party extensions break | Medium | @Observes remains operational; document migration path | + +--- + +## Summary of Affected Classes by Phase + +Legend: **NEW** = new file, **MOD** = modify existing file, **DEL** = remove file + +### Phase 0: Java 21 Baseline Upgrade + +| Action | File | Change | +|---|---|---| +| MOD | `pom.xml` | Set `maven.compiler.release` from `8` to `21`; upgrade checkstyle to 10.12+; upgrade Mockito to 5.x | +| MOD | `.github/workflows/ci.yml` | Update JDK matrix to 21 minimum | +| MOD | `.github/workflows/integration-tests.yml` | Update JDK matrix to 21 minimum | +| MOD | `.github/workflows/maven-jboss-snapshot-publish.yml` | Update JDK version | + +### Phase 2: Configuration Listener SPI + +| Action | File | Change | +|---|---|---| +| NEW | `core/spi/src/main/java/.../core/spi/ConfigurationListener.java` | New SPI interface with `onDescriptorLoaded(ArquillianDescriptor)` | +| NEW | `config/impl-base/src/main/java/.../config/impl/extension/ConfigurationListenerAdaptor.java` | Adaptor: `@Observes ArquillianDescriptor` → delegates to registered `ConfigurationListener` instances | +| MOD | `config/impl-base/src/main/java/.../config/impl/extension/ConfigExtension.java` | Register `ConfigurationListenerAdaptor` as observer | +| MOD | `container/impl-base/src/main/java/.../container/impl/client/container/ContainerRegistryCreator.java` | Implement `ConfigurationListener.onDescriptorLoaded()`; keep `@Observes` during transition | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/protocol/ProtocolRegistryCreator.java` | Implement `ConfigurationListener.onDescriptorLoaded()`; keep `@Observes` during transition | + +### Phase 3: Container Orchestration -- Reuse Existing SPIs + +| Action | File | Change | +|---|---|---| +| MOD | `container/impl-base/src/main/java/.../container/impl/client/container/ContainerLifecycleController.java` | Implement `ContainerMultiControlListener` (fan-out methods) and `ContainerControlListener` (per-container execution) | +| MOD | `container/impl-base/src/main/java/.../container/impl/client/deployment/ArchiveDeploymentExporter.java` | Implement `ContainerLifecycleListener.beforeDeploy()` | +| MOD | `container/impl-base/src/main/java/.../container/impl/ContainerExtension.java` | Register controller as listener instances | + +### Phase 4: Container Test Orchestration + +**Group A -- Test-to-Container Bridge:** + +| Action | File | Change | +|---|---|---| +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/ContainerEventController.java` | Implement `TestLifecycleListener` for non-interceptor methods (`beforeSuite`, `afterSuite`, `beforeClass`, `afterClass`); interceptor methods deferred to Phase 5 | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/ContainerTestExtension.java` | Register `ContainerEventController` as `TestLifecycleListener`; register new adaptors | + +**Group B -- Creator/Producer Observers:** + +| Action | File | Change | +|---|---|---| +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/ClientContainerControllerCreator.java` | Implement `ContainerMultiControlListener.setupContainers()`; call `manager.bind()` directly | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/deployment/ClientDeployerCreator.java` | Implement `ContainerMultiControlListener.setupContainers()`; call `manager.bind()` directly | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/ContainerContainerControllerCreator.java` | Implement `TestLifecycleListener.beforeSuite()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/ContainerRestarter.java` | Implement `TestLifecycleListener.beforeClass()` | + +**Group C -- Test Execution Chain:** + +| Action | File | Change | +|---|---|---| +| NEW | `container/test-spi/src/main/java/.../container/test/spi/TestExecutionListener.java` | New SPI interface with `executeTest()`, `executeLocal()`, `executeRemote()` | +| NEW | `container/test-impl-base/src/main/java/.../container/test/impl/execution/TestExecutionListenerAdaptor.java` | Adaptor: `@Observes Test/LocalExecutionEvent/RemoteExecutionEvent` → delegates to `TestExecutionListener` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/execution/ClientTestExecuter.java` | Implement `TestExecutionListener.executeTest()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/execution/LocalTestExecuter.java` | Implement `TestExecutionListener.executeLocal()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/execution/RemoteTestExecuter.java` | Implement `TestExecutionListener.executeRemote()` | + +**Group F -- Deployment Tooling (simple migration):** + +| Action | File | Change | +|---|---|---| +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/deployment/tool/ArchiveDeploymentToolingExporter.java` | Implement `ContainerLifecycleListener.beforeDeploy()` | + +**Group D -- Lifecycle Executors (NO CHANGES -- permanently `@Observes`):** + +| Action | File | Change | +|---|---|---| +| -- | `container/test-impl-base/src/main/java/.../container/test/impl/execution/BeforeLifecycleEventExecuter.java` | No change -- stays as `@Observes` | +| -- | `container/test-impl-base/src/main/java/.../container/test/impl/execution/AfterLifecycleEventExecuter.java` | No change -- stays as `@Observes` | +| -- | `container/test-impl-base/src/main/java/.../container/test/impl/execution/ClientBeforeAfterLifecycleEventExecuter.java` | No change -- stays as `@Observes` | + +### Phase 5: Interceptor Chain Evolution + +| Action | File | Change | +|---|---|---| +| NEW | `core/spi/src/main/java/.../core/spi/Interceptor.java` | New `@FunctionalInterface` with `intercept(T event, InterceptorChain chain)` | +| NEW | `core/spi/src/main/java/.../core/spi/InterceptorChain.java` | New interface with `proceed()` and `getEvent()` | +| MOD | `core/spi/src/main/java/.../core/spi/Manager.java` | Add `addInterceptor(Class, Interceptor, int priority)` | +| MOD | `core/impl-base/src/main/java/.../core/impl/ManagerImpl.java` | Implement `addInterceptor()`; unify old `EventContext` observers and new `Interceptor` into single priority-sorted chain in `fire()` | +| MOD | `core/impl-base/src/main/java/.../core/impl/EventContextImpl.java` | Evolve to support unified interceptor chain (old + new interleaved by priority) | +| MOD | `test/impl-base/src/main/java/.../test/impl/TestContextHandler.java` | Replace `@Observes(precedence=100) EventContext` with `manager.addInterceptor(..., 100)` | +| MOD | `container/impl-base/src/main/java/.../container/impl/client/ContainerDeploymentContextHandler.java` | Replace `@Observes EventContext` with `manager.addInterceptor(...)` | +| MOD | `container/impl-base/src/main/java/.../container/impl/client/container/DeploymentExceptionHandler.java` | Replace `@Observes EventContext` with `manager.addInterceptor(...)` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/ContainerEventController.java` | Replace interceptor methods (`EventContext`) with `manager.addInterceptor(...)` | + +### Phase 6: DI System Evolution + +| Action | File | Change | +|---|---|---| +| NEW | `core/spi/src/main/java/.../core/spi/ContextBinder.java` | New interface with `bind(scope, type, instance)` and `resolve(type)` | +| MOD | `config/impl-base/src/main/java/.../config/impl/extension/ConfigurationRegistrar.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/impl-base/src/main/java/.../container/impl/client/container/ContainerRegistryCreator.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/protocol/ProtocolRegistryCreator.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/deployment/DeploymentGenerator.java` | Replace `@Inject @ClassScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/ClientContainerControllerCreator.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/deployment/ClientDeployerCreator.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/ContainerContainerControllerCreator.java` | Replace `@Inject @ApplicationScoped InstanceProducer` with `ContextBinder.bind()` | + +### Phase 7: Peripheral Module Migration + +| Action | File | Change | +|---|---|---| +| MOD | `testenrichers/cdi/src/main/java/.../testenricher/cdi/container/BeanManagerProducer.java` | Implement `ManagerLifecycleListener.managerStarted()` | +| MOD | `testenrichers/initialcontext/src/main/java/.../testenricher/initialcontext/InitialContextProducer.java` | Implement `TestLifecycleListener.afterClass()` | +| MOD | `testenrichers/cdi/src/main/java/.../testenricher/cdi/CreationalContextDestroyer.java` | Replace `@Observes EventContext` with `Interceptor` | +| MOD | `protocols/servlet/src/main/java/.../protocol/servlet/runner/ServletContextRegistrar.java` | Implement `ManagerLifecycleListener.managerStarted()` | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/container/command/ContainerCommandObserver.java` | (Optional) Implement `CommandHandler` pattern | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/client/deployment/command/DeploymentCommandObserver.java` | (Optional) Implement `CommandHandler` pattern | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/enricher/resource/RemoteResourceCommandObserver.java` | (Optional) Implement `CommandHandler` pattern | + +### Phase 8: Cleanup and Final Integration + +| Action | File | Change | +|---|---|---| +| MOD | `test/impl-base/src/main/java/.../test/impl/EventTestRunnerAdaptor.java` | Add direct listener dispatch before event firing; dual-path for backward compat | +| DEL | `test/impl-base/src/main/java/.../test/impl/TestLifecycleListenerAdaptor.java` | Remove -- listeners now called directly by `EventTestRunnerAdaptor` | +| DEL | `test/impl-base/src/main/java/.../test/impl/TestEnrichmentListenerAdaptor.java` | Remove -- listeners called directly | +| DEL | `container/impl-base/src/main/java/.../container/impl/ContainerLifecycleListenerAdaptor.java` | Remove -- listeners called directly | +| DEL | `container/impl-base/src/main/java/.../container/impl/ContainerControlListenerAdaptor.java` | Remove -- listeners called directly | +| DEL | `container/impl-base/src/main/java/.../container/impl/ContainerMultiControlListenerAdaptor.java` | Remove -- listeners called directly | +| DEL | `core/impl-base/src/main/java/.../core/impl/ManagerLifecycleListenerAdaptor.java` | Remove -- listeners called directly | +| MOD | `core/spi/src/main/java/.../core/spi/LoadableExtension.java` | Add `listener()` and `interceptor()` methods to `ExtensionBuilder` | +| MOD | `test/impl-base/src/main/java/.../test/impl/TestExtension.java` | Remove adaptor observer registrations; add listener registrations | +| MOD | `container/impl-base/src/main/java/.../container/impl/ContainerExtension.java` | Remove adaptor observer registrations; add listener registrations | +| MOD | `core/impl-base/src/main/java/.../core/impl/CoreExtension.java` | Remove adaptor observer registration | +| MOD | `container/test-impl-base/src/main/java/.../container/test/impl/ContainerTestExtension.java` | Update all registrations to use `listener()`/`interceptor()` builder methods | + +--- + +## Summary of New Artifacts by Phase + +| Phase | New SPI Interface | New Adaptor | Module | +|---|---|---|---| +| 2 | `ConfigurationListener` | `ConfigurationListenerAdaptor` | core/spi, config/impl | +| 4 | `TestExecutionListener` | `TestExecutionListenerAdaptor` | container/test-spi, container/test-impl | +| 5 | `Interceptor`, `InterceptorChain` | (integrated into ManagerImpl) | core/spi, core/impl | +| 6 | `ContextBinder` | (passed by adaptors) | core/spi | +| 7 | `CommandHandler` (optional) | `CommandHandlerAdaptor` | container/test-spi | +| 8 | Extended `ExtensionBuilder` | -- | core/spi | diff --git a/docs/arch-notes/arquillian_2_plan_critique.md b/docs/arch-notes/arquillian_2_plan_critique.md new file mode 100644 index 000000000..fdc410ec6 --- /dev/null +++ b/docs/arch-notes/arquillian_2_plan_critique.md @@ -0,0 +1,306 @@ +# Arquillian 2.0 Migration Plan — Critical Review + +This document identifies gaps, untested assumptions, and structural risks in the +Event-to-SPI migration plan. Each issue is rated by severity and includes a +suggested resolution. + +--- + +## 1. The bindAndFire Hidden Event Chain (Severe) + +`InstanceProducer.set(value)` calls `manager.bindAndFire(scope, type, value)` in +`InstanceImpl.java:62-67`. This both stores the value in the scoped context AND +fires it as an event on the bus. The plan proposes `ConfigurationListener.onDescriptorLoaded()` +with an adaptor that `@Observes ArquillianDescriptor`, but if the event bus is +deprecated/removed in Phase 8, `InstanceProducer.set()` would stop firing the +event, and the adaptor would never trigger. + +**What the plan misses**: There is no strategy for replacing `bindAndFire`. Phase 6's +`ContextBinder` addresses the setter side but not the implicit event-firing side. +`InstanceImpl.set()` must be changed from `bindAndFire()` to `bind()` — but only +after every `@Observes SomeType` where `SomeType` is produced via `set()` has been +migrated to a listener. + +**Resolution**: See `docs/arch-notes/bindAndFire-replacement.md` for the full +replacement design. The `ArquillianDescriptor` appears to be the only value type +where `set()` triggers observer dispatch that downstream code depends on, which +limits the blast radius. Add an explicit step between Phase 2 and Phase 6 to +audit all `InstanceProducer.set()` calls and verify which ones have downstream +`@Observes` handlers. + +--- + +## 2. Injectable Anonymous Inner Classes (Severe) + +`ContainerLifecycleController` and `ContainerDeployController` create anonymous +`Operation` inner classes with `@Inject` fields that get DI-managed via +`injector.get().inject(operation)`. For example in +`ContainerLifecycleController.java:50-59`: + +```java +forEachContainer(new Operation() { + @Inject + private Event event; + @Override + public void perform(Container container) { + event.fire(new SetupContainer(container)); + } +}); +``` + +This is a deeply unusual DI pattern — ad-hoc anonymous objects get field injection +at runtime. A listener SPI implementation cannot replicate this because listeners +are not DI-managed. The plan says these controllers should "implement +`ContainerMultiControlListener`" but does not address how the injected +`Event` inside the anonymous operation would be replaced. + +**Resolution**: When migrating these controllers to listener implementations, +the controller must receive a `Manager` reference (via constructor injection or +method parameter) and call `manager.fire(new SetupContainer(container))` directly +instead of relying on an injected `Event` field. The anonymous `Operation` +classes should be replaced with lambdas that capture the Manager reference. +This is a Java 21 modernization opportunity. + +--- + +## 3. NonManagedObserver and TestResult Collection (Missed) + +`EventTestRunnerAdaptor.test()` at line 138 uses a pattern not mentioned in the plan: + +```java +manager.fire(new Test(testMethodExecutor), new NonManagedObserver() { + @Inject private Instance testResult; + @Override + public void fired(Test event) { + results.add(testResult.get()); + } +}); +``` + +This fires a `Test` event with an anonymous callback that gets injected and +collects the `TestResult` from the scoped context after all observers have run. +The `TestExecutionListener.executeTest()` proposed in Phase 4 has no equivalent +mechanism for returning a `TestResult`. This is the core of how test results +propagate back to the test framework runner. + +**Resolution**: The `TestExecutionListener` interface needs a return type or a +result-collection callback. Options: + +- `TestResult executeTest(TestMethodExecutor executor)` — listener returns the result +- Add a `TestResultCollector` parameter that listeners call with the result +- Keep `NonManagedObserver` as a supported pattern alongside the new listener system + +The third option is simplest for incremental migration. `EventTestRunnerAdaptor.test()` +can continue using `manager.fire(event, nonManagedObserver)` even after other +methods switch to direct listener dispatch. + +--- + +## 4. fireCustomLifecycle() is Unaddressed (Missed) + +`EventTestRunnerAdaptor.fireCustomLifecycle(TestLifecycleEvent)` fires arbitrary +custom event subtypes from JUnit 4 and JUnit 5: + +- `BeforeRules`, `AfterRules`, `RulesEnrichment` (JUnit 4) +- `RunModeEvent`, `BeforeTestExecutionEvent`, `MethodParameterProducerEvent` (JUnit 5) + +The `CustomEventListenerBehaviorTestCase` already on the branch documents that +custom events are invisible to typed listener SPI callbacks. The plan's Phase 8 +evolution of `EventTestRunnerAdaptor` does not address how `fireCustomLifecycle()` +will work if the event bus is deprecated. + +**Resolution**: `fireCustomLifecycle()` must continue to use `manager.fire(event)` +because the custom events are framework-specific subtypes that no listener SPI +can anticipate. This means the event bus cannot be fully removed — it must remain +operational for custom lifecycle events. The Phase 8 deprecation should be +"deprecated for core lifecycle events" rather than a blanket deprecation. + +--- + +## 5. No Listener Ordering Mechanism (Design Gap) + +The current `@Observes(precedence=N)` system provides explicit execution ordering. +The plan says listener ordering is "handled by registration order," but +registration order is determined by `ServiceLoader` discovery order — which +depends on classpath order and is non-deterministic across environments. + +This matters concretely: `ContainerLifecycleController` observing `SetupContainers` +must run before `ClientContainerControllerCreator` observing `SetupContainers`. +If both become `ContainerMultiControlListener` implementations, nothing guarantees +the order. + +**Resolution**: Add a priority mechanism to listener registration: + +```java +manager.addListener(ContainerMultiControlListener.class, controller, 100); +manager.addListener(ContainerMultiControlListener.class, creator, 50); +``` + +Or use an annotation/interface like `Prioritized` that listeners can implement. +This must be designed before Phase 3, where controllers and notification +listeners first coexist in the same listener type. + +--- + +## 6. Cascading Event Chains Cannot Be Expressed as Listeners (Architectural) + +The event sequence diagrams show cascading chains: + +``` +SetupContainers → [for each container] SetupContainer → BeforeSetup → container.setup() → AfterSetup +``` + +Three different listener SPIs fire in this chain: +- `ContainerMultiControlListener.setupContainers()` +- `ContainerControlListener.setupContainer(Container)` +- `ContainerLifecycleListener.beforeSetup(DeployableContainer)` / `afterSetup()` + +If `ContainerLifecycleController` implements both `ContainerMultiControlListener` +and `ContainerControlListener`, its `setupContainers()` method needs to fire +`SetupContainer` events that trigger `ContainerControlListener.setupContainer()` +callbacks. But without the event bus, who dispatches between the listener layers? + +The controller would need to manually iterate `manager.getListeners(ContainerControlListener.class)` +and call each one — duplicating the dispatch logic the Manager currently handles. + +**Resolution**: Accept that during the transition, the controllers will still use +`manager.fire()` internally to trigger the cascading chain. The listener SPI is +for external extensions to observe; the internal orchestration can continue using +the event bus until the final phase. Alternatively, introduce a +`ListenerDispatcher` utility that wraps the iteration pattern. + +--- + +## 7. Interceptor Type Matching with Inheritance (Underspecified) + +`ContainerDeploymentContextHandler` observes `EventContext`. +`ContainerControlEvent` is a base class; the actual fired events are subclasses +like `SetupContainer`, `StartContainer`, etc. The current event bus matches +observers against the event type hierarchy. + +The plan proposes `manager.addInterceptor(ContainerControlEvent.class, ...)` but +does not specify whether this matches subclasses. If it doesn't, the interceptor +silently stops working. If it does, the matching logic needs explicit design. + +**Resolution**: The `addInterceptor` implementation in `ManagerImpl` must walk the +event's class hierarchy and match against registered interceptor types, just as +the current observer resolution does. This must be specified in the Phase 5 +design and tested with a subclass-matching test case. + +--- + +## 8. ContainerTestExtension Duplicates TestExtension (Risk) + +`ContainerTestExtension` re-registers `SuiteContextImpl`, `ClassContextImpl`, +`TestContextImpl`, and `TestContextHandler` (lines 68-73) that `TestExtension` +also registers. The comment says "Copied from TestExtension." + +If both extensions register the same classes as listeners, you get double +invocations. Currently, the Manager handles duplicate observer registrations +(same class registered twice results in two instances), but the listener +registry uses `addListener()` which always appends. + +**Resolution**: Audit the extension loading to understand whether both +`TestExtension` and `ContainerTestExtension` are loaded simultaneously (they +are in different META-INF/services files, but both appear on the classpath in +integration tests). If they are, listener deduplication logic is needed — either +in `addListener()` or by ensuring only one extension registers listeners. + +--- + +## 9. Testing the Transition State (Verification Gap) + +During Phases 2-7, both the adaptor bridge and direct call paths exist. The plan +says "remove adaptor to prevent double dispatch" in Phase 8, but there is no +verification strategy for ensuring double dispatch does not happen during +intermediate phases. + +Each time a new listener SPI is introduced and an observer is migrated to +implement it, there is a window where both the old `@Observes` path and the new +listener path could fire. The existing test suite tests behavior (correct outcome), +not invocation counts. + +**Resolution**: Add invocation-counting test infrastructure. For each listener SPI, +write a test that: +1. Registers a counting listener via `manager.addListener()` +2. Fires the corresponding event +3. Asserts the listener was called exactly once, not twice + +This test catches double dispatch immediately. It should be written as part of +each phase, not deferred to Phase 8. + +--- + +## 10. Integration Tests May Not Run After Phase 0 (Practical) + +The integration tests (`-P integration-tests`) use container adaptors (WildFly, +Payara). If these adaptors do not support Java 21, the integration test suite +cannot run after Phase 0, leaving Phases 2-7 without end-to-end verification. + +**Resolution**: Before starting Phase 0, identify which container adaptors already +support Java 21. The Payara dependency is already at 7.2026.x which likely +supports Java 21. Verify WildFly adaptor compatibility. If needed, create a +minimal embedded container adaptor for integration testing that runs on Java 21. + +--- + +## 11. DeploymentScoped and ContainerScoped Not Addressed (Gap) + +Phase 6's `ContextBinder` interface shows `bind(scope, type, instance)` with +examples using `ApplicationScoped` and `ClassScoped`. But +`ContainerDeployController.deploy()` uses: + +- `@DeploymentScoped InstanceProducer` +- `@DeploymentScoped InstanceProducer` +- `@DeploymentScoped InstanceProducer` + +These scopes are activated/deactivated by interceptors +(`ContainerDeploymentContextHandler`). If interceptors are migrated in Phase 5 +and DI evolution happens in Phase 6, there is an ordering problem: Phase 6 needs +working interceptors from Phase 5, but the interceptors manage the context +lifecycle that Phase 6 is reworking. + +**Resolution**: The `ContextBinder` interface works with any scope annotation — it +just needs to be documented that `DeploymentScoped` and container-specific scopes +are supported. The ordering concern is valid: Phase 5 interceptors must be fully +working and tested before Phase 6 touches the DI system. Add this as an explicit +prerequisite in the phase dependency graph. + +--- + +## Summary: What Is Testable vs. What Is Not + +### Easily Testable +- Phase 0: full build passes with Java 21 +- Phase 1: already tested on branch +- Phase 2: simple adaptor bridge, standard listener dispatch +- Simple listener migrations in Phases 3 and 7 + +### Hard to Test +- Correct ordering of listener vs. observer invocations during transition (Phases 3-4) +- Absence of double dispatch during any intermediate state +- Interceptor type matching with inheritance (Phase 5) +- Cascading event chains working correctly in hybrid mode + +### Requires New Test Infrastructure +- `NonManagedObserver` result collection in the listener model +- `fireCustomLifecycle()` behavior with direct dispatch +- `bindAndFire` replacement — no existing tests verify the implicit event-from-set chain +- Invocation-counting tests for double dispatch prevention + +--- + +## Structural Assessment + +The plan's biggest structural weakness is that it treats the event bus, DI system, +and interceptor chain as separable concerns. They are deeply entangled: + +- `InstanceProducer.set()` fires events (DI → event bus coupling) +- Anonymous `Operation` classes get `@Inject` fields (event bus → DI coupling) +- Interceptors activate scopes that DI depends on (interceptor → DI coupling) +- `NonManagedObserver` gets injected after event dispatch (event bus → DI coupling) + +This means the phases cannot be truly independent. Changes in one layer ripple +through the others. The plan should acknowledge this and add cross-phase +integration checkpoints — points where the entire system is verified end-to-end, +not just the module that was changed. \ No newline at end of file diff --git a/docs/arch-notes/bindAndFire-replacement.md b/docs/arch-notes/bindAndFire-replacement.md new file mode 100644 index 000000000..697a3091b --- /dev/null +++ b/docs/arch-notes/bindAndFire-replacement.md @@ -0,0 +1,235 @@ +# bindAndFire Replacement: ArquillianDescriptor Configuration Flow + +This document shows how the implicit `bindAndFire` event chain for `ArquillianDescriptor` +works today, and how it would be replaced with explicit `ConfigurationListener` dispatch. + +--- + +## Current Flow: Implicit bindAndFire Chain + +The `ArquillianDescriptor` reaches `ContainerRegistryCreator` and `ProtocolRegistryCreator` +through a hidden coupling between the DI system and the event bus. `InstanceProducer.set()` +internally calls `manager.bindAndFire()`, which both stores the value in the scoped context +AND fires it as an event on the bus. + +```mermaid +sequenceDiagram + participant Manager as ManagerImpl + participant CfgReg as ConfigurationRegistrar + participant InstImpl as InstanceImpl (InstanceProducer) + participant AppCtx as ApplicationContext ObjectStore + participant EventBus as ManagerImpl fire() + participant EventCtx as EventContextImpl + participant CRC as ContainerRegistryCreator + participant PRC as ProtocolRegistryCreator + + Note over Manager: Manager.start() fires ManagerStarted + + Manager->>CfgReg: @Observes ManagerStarted + activate CfgReg + CfgReg->>CfgReg: loadConfiguration() + Note right of CfgReg: parse arquillian.xml, resolve placeholders + CfgReg->>InstImpl: descriptorInst.set(resolvedDesc) + activate InstImpl + + Note over InstImpl: InstanceProducer.set() calls manager.bindAndFire() + + InstImpl->>Manager: bindAndFire(ApplicationScoped, ArquillianDescriptor, resolvedDesc) + activate Manager + + Note over Manager,AppCtx: Step 1: bind into scope + + Manager->>AppCtx: bind(ArquillianDescriptor, resolvedDesc) + AppCtx-->>Manager: stored + + Note over Manager,EventBus: Step 2: fire the value AS an event — hidden coupling + + Manager->>EventBus: fire(resolvedDesc) + activate EventBus + EventBus->>EventBus: resolveObservers(ArquillianDescriptor.class) + + Note over EventBus: Finds @Observes ArquillianDescriptor in CRC and PRC + + EventBus->>EventCtx: new EventContextImpl(observers=[CRC, PRC], event=resolvedDesc) + activate EventCtx + EventCtx->>CRC: @Observes ArquillianDescriptor — createRegistry(resolvedDesc) + activate CRC + CRC->>CRC: validate config, create LocalContainerRegistry + CRC->>CRC: registry.set(reg) — binds ContainerRegistry into AppScope + CRC-->>EventCtx: return + deactivate CRC + + EventCtx->>PRC: @Observes ArquillianDescriptor — createRegistry(resolvedDesc) + activate PRC + PRC->>PRC: create ProtocolRegistry, discover protocols + PRC->>PRC: registry.set(reg) — binds ProtocolRegistry into AppScope + PRC-->>EventCtx: return + deactivate PRC + + EventCtx-->>EventBus: return + deactivate EventCtx + EventBus-->>Manager: return + deactivate EventBus + Manager-->>InstImpl: return + deactivate Manager + InstImpl-->>CfgReg: return + deactivate InstImpl + deactivate CfgReg +``` + +### Problems with Current Flow + +1. **Hidden coupling**: `InstanceProducer.set()` implicitly fires the value as an event — callers don't know this happens +2. **Untraceable**: Debugger shows `set()` → `bindAndFire()` → `fire()` → observer dispatch — indirect and surprising +3. **Fragile**: If `bindAndFire` is removed or the event bus is deprecated, `ContainerRegistryCreator` silently stops being invoked +4. **Testing**: No unit test verifies the `set()` → `fire()` → `@Observes` chain; tests mock around it + +--- + +## Proposed Flow: Explicit ConfigurationListener Dispatch + +Replace the implicit `fire(resolvedDesc)` with an explicit loop over registered +`ConfigurationListener` instances. The `InstanceProducer.set()` only binds into scope — +it no longer fires. The `ConfigurationRegistrar` itself is responsible for notifying listeners. + +```mermaid +sequenceDiagram + participant Manager as ManagerImpl + participant CfgReg as ConfigurationRegistrar + participant AppCtx as ApplicationContext ObjectStore + participant CRC as ContainerRegistryCreator + participant PRC as ProtocolRegistryCreator + participant Binder as ContextBinder + + Note over Manager: Manager.start() invokes ManagerLifecycleListener.managerStarted() + + Manager->>Manager: getListeners(ManagerLifecycleListener.class) + Manager->>CfgReg: managerStarted() + activate CfgReg + CfgReg->>CfgReg: loadConfiguration() + Note right of CfgReg: parse arquillian.xml, resolve placeholders + + Note over CfgReg,AppCtx: Step 1: bind into scope — NO implicit fire() + + CfgReg->>Manager: bind(ApplicationScoped, ArquillianDescriptor, resolvedDesc) + Manager->>AppCtx: store(ArquillianDescriptor, resolvedDesc) + AppCtx-->>Manager: stored + Manager-->>CfgReg: done + + Note over CfgReg,CRC: Step 2: explicitly notify ConfigurationListeners + + CfgReg->>Manager: getListeners(ConfigurationListener.class) + Manager-->>CfgReg: [CRC, PRC] + + CfgReg->>CRC: onDescriptorLoaded(resolvedDesc, binder) + activate CRC + CRC->>CRC: validate config, create LocalContainerRegistry + CRC->>Binder: bind(ApplicationScoped, ContainerRegistry, reg) + Binder->>AppCtx: store(ContainerRegistry, reg) + CRC-->>CfgReg: return + deactivate CRC + + CfgReg->>PRC: onDescriptorLoaded(resolvedDesc, binder) + activate PRC + PRC->>PRC: create ProtocolRegistry, discover protocols + PRC->>Binder: bind(ApplicationScoped, ProtocolRegistry, reg) + Binder->>AppCtx: store(ProtocolRegistry, reg) + PRC-->>CfgReg: return + deactivate PRC + + deactivate CfgReg + + Note over Manager: Configuration complete. ContainerRegistry and ProtocolRegistry in AppScope. +``` + +### Key Changes + +| Aspect | Current (bindAndFire) | Proposed (ConfigurationListener) | +|---|---|---| +| **Trigger** | `InstanceProducer.set()` implicitly fires event | `ConfigurationRegistrar` explicitly calls listeners | +| **Dispatch** | Event bus resolves `@Observes ArquillianDescriptor` | `manager.getListeners(ConfigurationListener.class)` | +| **Scope binding** | `bindAndFire()` does both bind + fire | `manager.bind()` for storage; listener call for notification | +| **Discoverability** | Must know that `set()` fires an event | Listener interface is explicit in the call chain | +| **Debuggability** | Stack trace goes through `fire()` → `EventContextImpl` → reflection | Direct method call: `listener.onDescriptorLoaded()` | +| **Dependency resolution** | Listeners use `@Inject Instance` | Listeners receive `ContextBinder` parameter | +| **Ordering** | `@Observes` precedence (implicit) | Registration order or explicit priority parameter | + +### ContextBinder Interface + +```java +public interface ContextBinder { + void bind(Class scope, Class type, T instance); + T resolve(Class type); +} +``` + +The `ContextBinder` replaces `@Inject @ApplicationScoped InstanceProducer` — listeners +receive it as a parameter and use it to bind values into scoped contexts without needing +DI-managed fields. + +### ConfigurationListener Interface + +```java +public interface ConfigurationListener { + default void onDescriptorLoaded(ArquillianDescriptor descriptor, + ContextBinder binder) throws Exception {} +} +``` + +### ManagerImpl.bindAndFire() Deprecation Path + +```java +// Current +public void bindAndFire(Class scope, Class type, T instance) { + bind(scope, type, instance); + fire(instance); // <-- this is the hidden coupling +} + +// Transitional: add bind-only method, keep bindAndFire for backward compat +public void bind(Class scope, Class type, T instance) { + // store into scoped context only, no event firing +} + +// InstanceProducer.set() changes from bindAndFire() to bind() +// Only after all @Observes-on-produced-value patterns are migrated to listeners +``` + +### What Must Change in InstanceImpl + +```java +// Current (InstanceImpl.java:62-67) +@Override +public void set(T value) { + if (scope == null) { + throw new IllegalStateException("..."); + } + manager.bindAndFire(scope, type, value); // fires event +} + +// Proposed +@Override +public void set(T value) { + if (scope == null) { + throw new IllegalStateException("..."); + } + manager.bind(scope, type, value); // bind only, no fire +} +``` + +**Warning**: This change breaks any code that depends on `set()` firing an event. +Every `@Observes SomeType` where `SomeType` is a value produced via `InstanceProducer.set()` +must be migrated to an explicit listener BEFORE `InstanceImpl` is changed. + +### Affected bindAndFire Patterns + +The following `InstanceProducer.set()` calls currently trigger implicit event dispatch +that downstream `@Observes` handlers depend on: + +| Producer | Value Type | Downstream Observer(s) | +|---|---|---| +| `ConfigurationRegistrar.descriptorInst.set()` | `ArquillianDescriptor` | `ContainerRegistryCreator`, `ProtocolRegistryCreator` | + +All other `InstanceProducer.set()` calls (e.g., `ContainerRegistryCreator.registry.set()`) +bind values that are consumed via `@Inject Instance.get()` — not via `@Observes`. +The `ArquillianDescriptor` is the **only** value type where `set()` triggers observer dispatch +that downstream code depends on. diff --git a/docs/arch-notes/event-sequence-diagrams.md b/docs/arch-notes/event-sequence-diagrams.md new file mode 100644 index 000000000..84fc5f43e --- /dev/null +++ b/docs/arch-notes/event-sequence-diagrams.md @@ -0,0 +1,206 @@ +# Arquillian Event Sequence Diagrams + +This document contains sequence diagrams for the various event flows in the Arquillian framework. + +## Core Event Flow + +The core event system in Arquillian is based on an observer pattern where events are fired by the `Manager` and processed by observers. Here's the sequence diagram for the core event flow: + +```mermaid +sequenceDiagram + participant Client as Client Code + participant Manager as ManagerImpl + participant EventContext as EventContextImpl + participant Observer as ObserverMethod + participant NonManagedObserver as NonManagedObserver + + Client->>Manager: fire(event) + Manager->>Manager: resolveObservers(event.class) + Manager->>Manager: resolveInterceptorObservers(event.class) + Manager->>EventContext: new EventContextImpl(manager, interceptors, observers, nonManagedObserver, event) + EventContext->>EventContext: proceed() + + alt Has Interceptors + EventContext->>Observer: invoke(manager, eventContext) + Observer-->>EventContext: proceed() + end + + loop For each observer + EventContext->>Observer: invoke(manager, event) + Observer->>Observer: resolveArguments(manager, event) + Observer->>Observer: method.invoke(target, arguments) + end + + alt Has NonManagedObserver + EventContext->>Manager: inject(nonManagedObserver) + EventContext->>NonManagedObserver: fired(event) + end + + EventContext-->>Manager: return + Manager-->>Client: return +``` + +This diagram shows how events are processed in Arquillian: + +1. A client fires an event through the Manager +2. The Manager resolves all observers and interceptors for the event type +3. An EventContext is created to handle the event processing +4. If there are interceptors, they are invoked first +5. Then all observers are invoked with the event +6. Finally, if there's a non-managed observer, it's injected and notified +7. Control returns to the client + +## Container Lifecycle Events + +Container lifecycle events represent the lifecycle of a container in Arquillian. Here's the sequence diagram: + +```mermaid +sequenceDiagram + participant Client as Client Code + participant Manager as Manager + participant Container as Container + participant DeployableContainer as DeployableContainer + + Client->>Manager: fire(SetupContainer) + Manager->>Manager: fire(BeforeSetup) + Manager->>Container: setup() + Manager->>Manager: fire(AfterSetup) + + Client->>Manager: fire(StartContainer) + Manager->>Manager: fire(BeforeStart) + Manager->>DeployableContainer: start() + Manager->>Manager: fire(AfterStart) + + Client->>Manager: fire(DeployDeployment) + Manager->>Manager: fire(BeforeDeploy) + Manager->>DeployableContainer: deploy() + Manager->>Manager: fire(AfterDeploy) + + Client->>Manager: fire(UnDeployDeployment) + Manager->>Manager: fire(BeforeUnDeploy) + Manager->>DeployableContainer: undeploy() + Manager->>Manager: fire(AfterUnDeploy) + + Client->>Manager: fire(StopContainer) + Manager->>Manager: fire(BeforeStop) + Manager->>DeployableContainer: stop() + Manager->>Manager: fire(AfterStop) + + Client->>Manager: fire(KillContainer) + Manager->>Manager: fire(BeforeKill) + Manager->>DeployableContainer: kill() + Manager->>Manager: fire(AfterKill) +``` + +This diagram shows the container lifecycle events: + +1. Container setup: SetupContainer → BeforeSetup → Container.setup() → AfterSetup +2. Container start: StartContainer → BeforeStart → DeployableContainer.start() → AfterStart +3. Deployment: DeployDeployment → BeforeDeploy → DeployableContainer.deploy() → AfterDeploy +4. Undeployment: UnDeployDeployment → BeforeUnDeploy → DeployableContainer.undeploy() → AfterUnDeploy +5. Container stop: StopContainer → BeforeStop → DeployableContainer.stop() → AfterStop +6. Container kill: KillContainer → BeforeKill → DeployableContainer.kill() → AfterKill + +## Test Lifecycle Events + +Test lifecycle events represent the execution of tests in Arquillian. Here's the sequence diagram: + +```mermaid +sequenceDiagram + participant Client as Test Runner + participant Manager as Manager + participant TestInstance as Test Instance + + Client->>Manager: fire(BeforeSuite) + + loop For each test class + Client->>Manager: fire(BeforeClass) + + loop For each test method + Client->>Manager: fire(Before) + Client->>Manager: fire(BeforeEnrichment) + Manager->>TestInstance: enrich test instance + Client->>Manager: fire(AfterEnrichment) + + Client->>Manager: fire(Test) + alt Local Execution + Manager->>Manager: fire(LocalExecutionEvent) + else Remote Execution + Manager->>Manager: fire(RemoteExecutionEvent) + end + + Client->>Manager: fire(After) + end + + Client->>Manager: fire(AfterClass) + end + + Client->>Manager: fire(AfterSuite) +``` + +This diagram shows the test lifecycle events: + +1. Suite lifecycle: BeforeSuite → (test execution) → AfterSuite +2. Class lifecycle: BeforeClass → (test methods execution) → AfterClass +3. Test method lifecycle: Before → BeforeEnrichment → AfterEnrichment → Test → (LocalExecutionEvent or RemoteExecutionEvent) → After + +## Deployment Events + +Deployment events represent the deployment of artifacts to containers. Here's the sequence diagram: + +```mermaid +sequenceDiagram + participant Client as Client Code + participant Manager as Manager + participant Container as Container + participant DeployableContainer as DeployableContainer + + Client->>Manager: fire(DeployManagedDeployments) + + loop For each deployment + Manager->>Manager: fire(DeployDeployment) + Manager->>Manager: fire(BeforeDeploy) + Manager->>DeployableContainer: deploy() + Manager->>Manager: fire(AfterDeploy) + end + + Client->>Manager: fire(UnDeployManagedDeployments) + + loop For each deployment + Manager->>Manager: fire(UnDeployDeployment) + Manager->>Manager: fire(BeforeUnDeploy) + Manager->>DeployableContainer: undeploy() + Manager->>Manager: fire(AfterUnDeploy) + end +``` + +This diagram shows the deployment events: + +1. Managed deployments: DeployManagedDeployments → (for each deployment) → DeployDeployment → BeforeDeploy → deploy() → AfterDeploy +2. Managed undeployments: UnDeployManagedDeployments → (for each deployment) → UnDeployDeployment → BeforeUnDeploy → undeploy() → AfterUnDeploy + +## Execution Events + +Execution events represent the execution of test methods. Here's the sequence diagram: + +```mermaid +sequenceDiagram + participant Client as Test Runner + participant Manager as Manager + participant Executor as TestMethodExecutor + + Client->>Manager: fire(Test) + + alt Local Execution + Manager->>Manager: fire(LocalExecutionEvent) + Manager->>Executor: execute() + else Remote Execution + Manager->>Manager: fire(RemoteExecutionEvent) + Manager->>Executor: execute() (via protocol) + end +``` + +This diagram shows the execution events: + +1. Local execution: Test → LocalExecutionEvent → execute() +2. Remote execution: Test → RemoteExecutionEvent → execute() (via protocol) \ No newline at end of file diff --git a/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerAdaptor.java b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerAdaptor.java new file mode 100644 index 000000000..5730c53fc --- /dev/null +++ b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerAdaptor.java @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.impl; + +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.test.spi.TestEnrichmentListener; +import org.jboss.arquillian.test.spi.event.enrichment.AfterEnrichment; +import org.jboss.arquillian.test.spi.event.enrichment.BeforeEnrichment; + +/** + * Adaptor that bridges test enrichment events to the {@link TestEnrichmentListener} SPI. + * + * @author Arquillian + */ +public class TestEnrichmentListenerAdaptor { + + @Inject + private Instance manager; + + public void onBeforeEnrichment(@Observes BeforeEnrichment event) throws Exception { + for (TestEnrichmentListener listener : manager.get().getListeners(TestEnrichmentListener.class)) { + listener.beforeEnrichment(event.getInstance(), event.getMethod()); + } + } + + public void onAfterEnrichment(@Observes AfterEnrichment event) throws Exception { + for (TestEnrichmentListener listener : manager.get().getListeners(TestEnrichmentListener.class)) { + listener.afterEnrichment(event.getInstance(), event.getMethod()); + } + } +} diff --git a/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestExtension.java b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestExtension.java index 120e10132..cc519ef28 100644 --- a/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestExtension.java +++ b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestExtension.java @@ -39,6 +39,8 @@ public void register(ExtensionBuilder builder) { builder.service(TestEnricher.class, ArquillianResourceTestEnricher.class); builder.observer(TestContextHandler.class) - .observer(TestInstanceEnricher.class); + .observer(TestInstanceEnricher.class) + .observer(TestLifecycleListenerAdaptor.class) + .observer(TestEnrichmentListenerAdaptor.class); } } diff --git a/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestLifecycleListenerAdaptor.java b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestLifecycleListenerAdaptor.java new file mode 100644 index 000000000..44ee52fde --- /dev/null +++ b/test/impl-base/src/main/java/org/jboss/arquillian/test/impl/TestLifecycleListenerAdaptor.java @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.impl; + +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.test.spi.TestLifecycleListener; +import org.jboss.arquillian.test.spi.event.suite.After; +import org.jboss.arquillian.test.spi.event.suite.AfterClass; +import org.jboss.arquillian.test.spi.event.suite.AfterSuite; +import org.jboss.arquillian.test.spi.event.suite.Before; +import org.jboss.arquillian.test.spi.event.suite.BeforeClass; +import org.jboss.arquillian.test.spi.event.suite.BeforeSuite; + +/** + * Adaptor that bridges test suite lifecycle events to the {@link TestLifecycleListener} SPI. + * + * @author Arquillian + */ +public class TestLifecycleListenerAdaptor { + + @Inject + private Instance manager; + + public void onBeforeSuite(@Observes BeforeSuite event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.beforeSuite(); + } + } + + public void onAfterSuite(@Observes AfterSuite event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.afterSuite(); + } + } + + public void onBeforeClass(@Observes BeforeClass event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.beforeClass(event.getTestClass(), event.getExecutor()); + } + } + + public void onAfterClass(@Observes AfterClass event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.afterClass(event.getTestClass(), event.getExecutor()); + } + } + + public void onBefore(@Observes Before event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.before(event.getTestInstance(), event.getTestMethod(), event.getExecutor()); + } + } + + public void onAfter(@Observes After event) throws Exception { + for (TestLifecycleListener listener : manager.get().getListeners(TestLifecycleListener.class)) { + listener.after(event.getTestInstance(), event.getTestMethod(), event.getExecutor()); + } + } +} diff --git a/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/CustomTestLifecycleEventBehaviorTestCase.java b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/CustomTestLifecycleEventBehaviorTestCase.java new file mode 100644 index 000000000..46d9585c9 --- /dev/null +++ b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/CustomTestLifecycleEventBehaviorTestCase.java @@ -0,0 +1,260 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.impl; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.test.spi.LifecycleMethodExecutor; +import org.jboss.arquillian.test.spi.TestLifecycleListener; +import org.jboss.arquillian.test.spi.event.suite.Before; +import org.jboss.arquillian.test.spi.event.suite.BeforeTestLifecycleEvent; +import org.jboss.arquillian.test.test.AbstractTestTestBase; +import org.junit.Assert; +import org.junit.Test; + +/** + * Documents the behavior of the {@link TestLifecycleListener} SPI when custom + * {@link BeforeTestLifecycleEvent} subtypes are fired — as done by JUnit 4 + * ({@code BeforeRules}, {@code AfterRules}, {@code RulesEnrichment}) and + * JUnit 5 ({@code RunModeEvent}, {@code BeforeTestExecutionEvent}). + * + *

Key Findings

+ *
    + *
  1. {@code TestLifecycleListenerAdaptor} observes specific concrete types + * ({@code Before}, {@code After}, etc.), NOT the abstract base class + * {@code BeforeTestLifecycleEvent} or {@code TestLifecycleEvent}.
  2. + *
  3. Therefore, custom events like JUnit 4's {@code BeforeRules} (which + * extends {@code BeforeTestLifecycleEvent}) are invisible to + * {@code TestLifecycleListener}. They only reach {@code @Observes} + * observers.
  4. + *
  5. This is a known gap: the listener SPI does not yet provide a hook for + * framework-specific custom lifecycle events. A general-purpose hook + * (e.g. {@code fireCustomLifecycle} equivalent in the SPI) would be + * needed to bridge this gap.
  6. + *
  7. The standard {@code Before} event DOES trigger {@code TestLifecycleListener}, + * confirming the adaptor works correctly for known event types.
  8. + *
+ */ +public class CustomTestLifecycleEventBehaviorTestCase extends AbstractTestTestBase { + + // ------------------------------------------------------------------------- + // Simulated custom events — mirrors what JUnit 4 / JUnit 5 do externally + // ------------------------------------------------------------------------- + + /** + * Simulates JUnit 4's {@code BeforeRules} event, which extends + * {@code BeforeTestLifecycleEvent} with extra context (statement, TestClass). + */ + static class SimulatedBeforeRules extends BeforeTestLifecycleEvent { + SimulatedBeforeRules(Object testInstance, Method testMethod) { + super(testInstance, testMethod, LifecycleMethodExecutor.NO_OP); + } + } + + /** + * Simulates JUnit 5's {@code RunModeEvent}, which also extends + * {@code BeforeTestLifecycleEvent}. + */ + static class SimulatedRunModeEvent extends BeforeTestLifecycleEvent { + private boolean runAsClient = true; + + SimulatedRunModeEvent(Object testInstance, Method testMethod) { + super(testInstance, testMethod); + } + + boolean isRunAsClient() { + return runAsClient; + } + } + + // ------------------------------------------------------------------------- + // Traditional @Observes observer — must continue working + // ------------------------------------------------------------------------- + + static class CustomEventLegacyObserver { + final List received = new ArrayList(); + + /** + * Observes the abstract supertype — receives BOTH custom events and the + * standard {@code Before} event (since Before extends + * BeforeTestLifecycleEvent). + */ + public void onAnyBeforeLifecycle(@Observes BeforeTestLifecycleEvent event) { + received.add(event); + } + } + + @Override + protected void addExtensions(List> extensions) { + extensions.add(TestLifecycleListenerAdaptor.class); + extensions.add(CustomEventLegacyObserver.class); + } + + /** + * FINDING 1: The standard {@code Before} event triggers + * {@code TestLifecycleListener.before()} — the listener SPI works for + * known event types. + */ + @Test + public void standardBeforeEventTriggersTestLifecycleListener() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + Method method = getClass().getMethod( + "standardBeforeEventTriggersTestLifecycleListener"); + fire(new Before(this, method, LifecycleMethodExecutor.NO_OP)); + + Assert.assertTrue( + "TestLifecycleListener.before() should be called for standard Before event", + listener.beforeCalled); + } + + /** + * FINDING 2: {@code SimulatedBeforeRules} (extending + * {@code BeforeTestLifecycleEvent}) does NOT trigger + * {@code TestLifecycleListener.before()}. + *

+ * The adaptor observes {@code Before} specifically, not the abstract + * {@code BeforeTestLifecycleEvent} supertype. + * This is the GAP: JUnit 4's BeforeRules / AfterRules, JUnit 5's RunModeEvent, + * BeforeTestExecutionEvent etc. are invisible to the listener SPI. + */ + @Test + public void customBeforeRulesEventDoesNotTriggerTestLifecycleListener() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + Method method = getClass().getMethod( + "customBeforeRulesEventDoesNotTriggerTestLifecycleListener"); + fire(new SimulatedBeforeRules(this, method)); + + Assert.assertFalse( + "TestLifecycleListener.before() must NOT be called for custom BeforeRules event " + + "(gap: custom subtypes of BeforeTestLifecycleEvent bypass the listener SPI)", + listener.beforeCalled); + } + + /** + * FINDING 3: {@code SimulatedRunModeEvent} (extending + * {@code BeforeTestLifecycleEvent}) also does NOT trigger + * {@code TestLifecycleListener.before()}. + *

+ * Confirms the same gap applies to JUnit 5 custom events. + */ + @Test + public void customRunModeEventDoesNotTriggerTestLifecycleListener() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + Method method = getClass().getMethod( + "customRunModeEventDoesNotTriggerTestLifecycleListener"); + fire(new SimulatedRunModeEvent(this, method)); + + Assert.assertFalse( + "TestLifecycleListener.before() must NOT be called for custom RunModeEvent " + + "(gap: custom subtypes of BeforeTestLifecycleEvent bypass the listener SPI)", + listener.beforeCalled); + } + + /** + * FINDING 4: Custom events STILL reach {@code @Observes} observers of the + * correct type — the legacy mechanism is unaffected. + *

+ * An observer of {@code BeforeTestLifecycleEvent} (the supertype) receives + * both {@code SimulatedBeforeRules} and the standard {@code Before} event, + * because {@code @Observes} uses assignability. + */ + @Test + public void customEventsStillReachLegacyObserversByTypeCompatibility() throws Exception { + CustomEventLegacyObserver observer = + getManager().getExtension(CustomEventLegacyObserver.class); + Method method = getClass().getMethod( + "customEventsStillReachLegacyObserversByTypeCompatibility"); + + // Standard Before event — reaches observer via BeforeTestLifecycleEvent + fire(new Before(this, method, LifecycleMethodExecutor.NO_OP)); + + // Custom BeforeRules — also reaches the same observer + fire(new SimulatedBeforeRules(this, method)); + + // Custom RunModeEvent — also reaches the same observer + fire(new SimulatedRunModeEvent(this, method)); + + Assert.assertEquals( + "Observer of BeforeTestLifecycleEvent should receive Before + BeforeRules + RunModeEvent", + 3, observer.received.size()); + Assert.assertTrue("First event should be Before", + observer.received.get(0) instanceof Before); + Assert.assertTrue("Second event should be SimulatedBeforeRules", + observer.received.get(1) instanceof SimulatedBeforeRules); + Assert.assertTrue("Third event should be SimulatedRunModeEvent", + observer.received.get(2) instanceof SimulatedRunModeEvent); + } + + /** + * FINDING 5: The GAP in concrete numbers — when all three events fire, + * the listener SPI callback count vs the legacy observer count differs. + *

+ * This confirms the asymmetry: listener SPI coverage is strict (exact type), + * while {@code @Observes} coverage is polymorphic (assignability). + */ + @Test + public void demonstratesGapBetweenListenerSpiAndLegacyObserver() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + CustomEventLegacyObserver observer = + getManager().getExtension(CustomEventLegacyObserver.class); + + Method method = getClass().getMethod( + "demonstratesGapBetweenListenerSpiAndLegacyObserver"); + + fire(new Before(this, method, LifecycleMethodExecutor.NO_OP)); + fire(new SimulatedBeforeRules(this, method)); + fire(new SimulatedRunModeEvent(this, method)); + + // Legacy observer receives all 3 (polymorphic via BeforeTestLifecycleEvent) + Assert.assertEquals( + "Legacy @Observes receives all 3 events (polymorphic assignability)", + 3, observer.received.size()); + + // Listener SPI receives only 1 (the standard Before) + Assert.assertEquals( + "Listener SPI before() is called only once — only for the standard Before event. " + + "Custom subtypes (BeforeRules, RunModeEvent) are invisible to the listener SPI.", + 1, listener.beforeCallCount); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static class TrackingTestLifecycleListener implements TestLifecycleListener { + boolean beforeCalled = false; + int beforeCallCount = 0; + + @Override + public void before(Object testInstance, Method testMethod, + LifecycleMethodExecutor executor) throws Exception { + beforeCalled = true; + beforeCallCount++; + } + } +} diff --git a/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerTestCase.java b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerTestCase.java new file mode 100644 index 000000000..85b1ae5c8 --- /dev/null +++ b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestEnrichmentListenerTestCase.java @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.impl; + +import java.lang.reflect.Method; +import java.util.List; +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.test.spi.TestEnrichmentListener; +import org.jboss.arquillian.test.spi.event.enrichment.AfterEnrichment; +import org.jboss.arquillian.test.spi.event.enrichment.BeforeEnrichment; +import org.jboss.arquillian.test.test.AbstractTestTestBase; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that {@link TestEnrichmentListener} instances registered via + * {@link Manager#addListener(Class, Object)} are notified for enrichment events. + */ +public class TestEnrichmentListenerTestCase extends AbstractTestTestBase { + + @Override + protected void addExtensions(List> extensions) { + extensions.add(TestEnrichmentListenerAdaptor.class); + } + + @Test + public void shouldNotifyListenerOnBeforeAndAfterEnrichment() throws Exception { + TrackingTestEnrichmentListener listener = new TrackingTestEnrichmentListener(); + getManager().addListener(TestEnrichmentListener.class, listener); + + Object testInstance = this; + Method testMethod = TestEnrichmentListenerTestCase.class + .getMethod("shouldNotifyListenerOnBeforeAndAfterEnrichment"); + + fire(new BeforeEnrichment(testInstance, testMethod)); + Assert.assertTrue("beforeEnrichment() should have been called", listener.beforeEnrichment); + Assert.assertSame("testInstance should be passed to beforeEnrichment()", testInstance, listener.instance); + Assert.assertSame("testMethod should be passed to beforeEnrichment()", testMethod, listener.method); + + fire(new AfterEnrichment(testInstance, testMethod)); + Assert.assertTrue("afterEnrichment() should have been called", listener.afterEnrichment); + } + + private static class TrackingTestEnrichmentListener implements TestEnrichmentListener { + boolean beforeEnrichment = false; + boolean afterEnrichment = false; + Object instance = null; + Method method = null; + + @Override + public void beforeEnrichment(Object testInstance, Method testMethod) throws Exception { + beforeEnrichment = true; + instance = testInstance; + method = testMethod; + } + + @Override + public void afterEnrichment(Object testInstance, Method testMethod) throws Exception { + afterEnrichment = true; + } + } +} diff --git a/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestLifecycleListenerTestCase.java b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestLifecycleListenerTestCase.java new file mode 100644 index 000000000..c32a9d8e2 --- /dev/null +++ b/test/impl-base/src/test/java/org/jboss/arquillian/test/impl/TestLifecycleListenerTestCase.java @@ -0,0 +1,136 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.impl; + +import java.lang.reflect.Method; +import java.util.List; +import org.jboss.arquillian.core.spi.Manager; +import org.jboss.arquillian.test.spi.LifecycleMethodExecutor; +import org.jboss.arquillian.test.spi.TestClass; +import org.jboss.arquillian.test.spi.TestLifecycleListener; +import org.jboss.arquillian.test.spi.event.suite.After; +import org.jboss.arquillian.test.spi.event.suite.AfterClass; +import org.jboss.arquillian.test.spi.event.suite.AfterSuite; +import org.jboss.arquillian.test.spi.event.suite.Before; +import org.jboss.arquillian.test.spi.event.suite.BeforeClass; +import org.jboss.arquillian.test.spi.event.suite.BeforeSuite; +import org.jboss.arquillian.test.test.AbstractTestTestBase; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that {@link TestLifecycleListener} instances registered via + * {@link Manager#addListener(Class, Object)} are notified for test suite lifecycle events. + */ +public class TestLifecycleListenerTestCase extends AbstractTestTestBase { + + @Override + protected void addExtensions(List> extensions) { + extensions.add(TestLifecycleListenerAdaptor.class); + } + + @Test + public void shouldNotifyListenerOnBeforeAndAfterSuite() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + fire(new BeforeSuite()); + Assert.assertTrue("beforeSuite() should have been called", listener.beforeSuite); + + fire(new AfterSuite()); + Assert.assertTrue("afterSuite() should have been called", listener.afterSuite); + } + + @Test + public void shouldNotifyListenerOnBeforeAndAfterClass() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + Class clazz = TestLifecycleListenerTestCase.class; + fire(new BeforeClass(clazz, LifecycleMethodExecutor.NO_OP)); + Assert.assertTrue("beforeClass() should have been called", listener.beforeClass); + Assert.assertSame("underlying class should be passed to beforeClass()", + clazz, listener.testClass.getJavaClass()); + + fire(new AfterClass(clazz, LifecycleMethodExecutor.NO_OP)); + Assert.assertTrue("afterClass() should have been called", listener.afterClass); + } + + @Test + public void shouldNotifyListenerOnBeforeAndAfterTest() throws Exception { + TrackingTestLifecycleListener listener = new TrackingTestLifecycleListener(); + getManager().addListener(TestLifecycleListener.class, listener); + + Object testInstance = this; + Method testMethod = TestLifecycleListenerTestCase.class + .getMethod("shouldNotifyListenerOnBeforeAndAfterTest"); + + fire(new Before(testInstance, testMethod, LifecycleMethodExecutor.NO_OP)); + Assert.assertTrue("before() should have been called", listener.before); + Assert.assertSame("testInstance should be passed to before()", testInstance, listener.testInstance); + Assert.assertSame("testMethod should be passed to before()", testMethod, listener.testMethod); + + fire(new After(testInstance, testMethod, LifecycleMethodExecutor.NO_OP)); + Assert.assertTrue("after() should have been called", listener.after); + } + + private static class TrackingTestLifecycleListener implements TestLifecycleListener { + boolean beforeSuite = false; + boolean afterSuite = false; + boolean beforeClass = false; + boolean afterClass = false; + boolean before = false; + boolean after = false; + TestClass testClass = null; + Object testInstance = null; + Method testMethod = null; + + @Override + public void beforeSuite() throws Exception { + beforeSuite = true; + } + + @Override + public void afterSuite() throws Exception { + afterSuite = true; + } + + @Override + public void beforeClass(TestClass tc, LifecycleMethodExecutor executor) throws Exception { + beforeClass = true; + testClass = tc; + } + + @Override + public void afterClass(TestClass tc, LifecycleMethodExecutor executor) throws Exception { + afterClass = true; + } + + @Override + public void before(Object instance, Method method, LifecycleMethodExecutor executor) throws Exception { + before = true; + testInstance = instance; + testMethod = method; + } + + @Override + public void after(Object instance, Method method, LifecycleMethodExecutor executor) throws Exception { + after = true; + } + } +} diff --git a/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestEnrichmentListener.java b/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestEnrichmentListener.java new file mode 100644 index 000000000..dc6865eff --- /dev/null +++ b/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestEnrichmentListener.java @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.spi; + +import java.lang.reflect.Method; + +/** + * Listener SPI for the test enrichment event package + * ({@code org.jboss.arquillian.test.spi.event.enrichment}). + * + *

Register instances via {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface TestEnrichmentListener { + + default void beforeEnrichment(Object testInstance, Method testMethod) throws Exception { + } + + default void afterEnrichment(Object testInstance, Method testMethod) throws Exception { + } +} diff --git a/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestLifecycleListener.java b/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestLifecycleListener.java new file mode 100644 index 000000000..57afe070b --- /dev/null +++ b/test/spi/src/main/java/org/jboss/arquillian/test/spi/TestLifecycleListener.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.arquillian.test.spi; + +import java.lang.reflect.Method; + +/** + * Listener SPI for the test suite lifecycle event package + * ({@code org.jboss.arquillian.test.spi.event.suite}). + * + *

Register instances via {@link org.jboss.arquillian.core.spi.Manager#addListener(Class, Object)}. + * Each method defaults to a no-op so implementors only override what they need. + * + * @author Arquillian + */ +public interface TestLifecycleListener { + + default void beforeSuite() throws Exception { + } + + default void afterSuite() throws Exception { + } + + default void beforeClass(TestClass testClass, LifecycleMethodExecutor executor) throws Exception { + } + + default void afterClass(TestClass testClass, LifecycleMethodExecutor executor) throws Exception { + } + + default void before(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) throws Exception { + } + + default void after(Object testInstance, Method testMethod, LifecycleMethodExecutor executor) throws Exception { + } +}