diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e5d21f --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# stiletto +Launch system that allows classes to be invoked in phases to make sure all dependencies are satisfied. + +## Features +* Custom phases +* Builder-style + +## Installation + +#### Using Maven +Add the following to your `pom.xml`-file: + +```xml + + com.github.pyknic + rocket + 1.0.0 + +``` + +#### Using Gradle +Add the following to your `build.gradle`-file: +```gradle +compile group: 'com.github.pyknic', name: 'rocket', version: '1.0.0' +``` + +## Usage +The launch system follows a builder pattern where the two interfaces `Rocket` and `RocketBuilder` are central. + +### Configuration +The builder pattern allows Rocket to be built by appending types available for launching. If the dependency graph is incomplete or contains cyclic dependencies when the `RocketBuilder.build()`-method is invoked, then an `RocketException` is thrown. + +```java +// Create a new Launcher with a number of instances. +Rocket rocket = Rocket.builder(Phase.class) + .with(foo) + .with(bar) + .with(baz) + .build(); +``` + +### Define Phases +Phases are defined by creating an `enum` class and passing it to the `Rocket.builder(...)`-method. + +```java +// Define an enum with three phases. +enum Phase { + INIT, + UPDATE, + DESTROY +} +``` + +### Add Action +Actions can be added to a class by adding the `@Execute`-annotation to the method. If the method takes any parameters, they will be injected automatically. Methods with the same phase will be invoked in a order that guarantees that all the arguments have already passed that stage. + +```java +class ExampleComponent { + + @Execute("init") + void onInit() { + ... + } + + @Execute("update") + void onUpdate(OtherComponent other) { + ... + } + +} +``` + +## License +Copyright 2017 Emil Forslund + +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](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. diff --git a/license_header.txt b/license_header.txt new file mode 100644 index 0000000..b902721 --- /dev/null +++ b/license_header.txt @@ -0,0 +1,16 @@ + +Copyright (c) ${currentYear}, Emil Forslund. All Rights Reserved. + +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. + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e064a79 --- /dev/null +++ b/pom.xml @@ -0,0 +1,208 @@ + + + 4.0.0 + + com.github.pyknic + rocket + 1.0.0-SNAPSHOT + + Rocket + + Launch system that allows classes to be invoked in phases to make sure + all dependencies are satisfied. + + https://www.github.com/Pyknic/rocket/ + + + UTF-8 + 1.8 + 1.8 + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + Emil Forslund + http://github.com/pyknic + + + + + Emil Forslund + emil@speedment.com + Speedment + http://www.speedment.com + + + + + + org.junit.jupiter + junit-jupiter-api + 5.0.0-M3 + test + + + + + scm:git:git@github.com:pyknic/rocket.git + scm:git:git@github.com:pyknic/rocket.git + git@github.com:pyknic/rocket.git + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + -Xlint:all + true + true + + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + true + + + + com.github.pyknic.rocket + + + + + + + + + + ossrh + + + + com.mycila + license-maven-plugin + 3.0 + +
license_header.txt
+ + 2017 + + + **/README + **/LICENSE + **/*.xml + **/*.iml + **/package-info.java + src/test/resources/** + src/main/resources/** + +
+ + + generate-sources + + format + + + +
+ + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + true + com.github.pyknic.rocket.internal + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + true + ${gpg.keyname} + ${gpg.passphrase} + ${gpg.executable} + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + + default-deploy + deploy + + deploy + + + + + ossrh + https://oss.sonatype.org/ + true + + +
+
+
+
+
diff --git a/src/main/java/com/github/pyknic/rocket/Execute.java b/src/main/java/com/github/pyknic/rocket/Execute.java new file mode 100644 index 0000000..6212c91 --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/Execute.java @@ -0,0 +1,45 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that signals that a method should be invoked as part of a phase in + * the launcher. + * + * @author Emil Forslund + * @since 1.0.0 + */ +@Inherited +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Execute { + + /** + * Name of the phase to execute in. This should correspond to the result of + * the {@link Enum#name()} method for that phase. + * + * @return the phase + */ + String value(); + +} diff --git a/src/main/java/com/github/pyknic/rocket/Rocket.java b/src/main/java/com/github/pyknic/rocket/Rocket.java new file mode 100644 index 0000000..945bfa8 --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/Rocket.java @@ -0,0 +1,49 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket; + +import com.github.pyknic.rocket.internal.RocketBuilderImpl; + +/** + * Rocker Launcher system. + * + * @param the phase category enum + * + * @author Emil Forslund + * @since 1.0.0 + */ +public interface Rocket> { + + /** + * Creates a new {@link RocketBuilder}. + * + * @param the phases enum type + * @param phasesEnum enum representing the phases available + * @return the new builder + */ + static > RocketBuilder builder(Class phasesEnum) { + return new RocketBuilderImpl<>(phasesEnum); + } + + /** + * Invoke all the methods as part of the specified phase. + * + * @param phase the phase to invoke + */ + void launch(E phase); + +} diff --git a/src/main/java/com/github/pyknic/rocket/RocketBuilder.java b/src/main/java/com/github/pyknic/rocket/RocketBuilder.java new file mode 100644 index 0000000..80fe262 --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/RocketBuilder.java @@ -0,0 +1,46 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket; + +/** + * Builder for the {@link Rocket} interface. + * + * @param the phase category enum + * + * @author Emil Forslund + * @since 1.0.0 + */ +public interface RocketBuilder> { + + /** + * Add the specified instance to the launcher system. The instance will be + * scanned for methods with the {@link Execute}-annotation. + * + * @param the type of the instance + * @param instance the instance to add + * @return a reference to this builder + */ + RocketBuilder with(T instance); + + /** + * Builds the {@link Rocket} instance, resolving all the dependencies. + * + * @return the built {@link Rocket} + */ + Rocket build(); + +} \ No newline at end of file diff --git a/src/main/java/com/github/pyknic/rocket/RocketException.java b/src/main/java/com/github/pyknic/rocket/RocketException.java new file mode 100644 index 0000000..c1e82ee --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/RocketException.java @@ -0,0 +1,35 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket; + +/** + * Exception thrown if something went wrong while constructing the launcher + * system. + * + * @author Emil Forslund + * @since 1.0.0 + */ +public final class RocketException extends RuntimeException { + + public RocketException(String message) { + super(message); + } + + public RocketException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/github/pyknic/rocket/internal/RocketBuilderImpl.java b/src/main/java/com/github/pyknic/rocket/internal/RocketBuilderImpl.java new file mode 100644 index 0000000..d28bdb9 --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/internal/RocketBuilderImpl.java @@ -0,0 +1,185 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket.internal; + +import com.github.pyknic.rocket.Execute; +import com.github.pyknic.rocket.Rocket; +import com.github.pyknic.rocket.RocketBuilder; +import com.github.pyknic.rocket.RocketException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static com.github.pyknic.rocket.internal.util.ReflectionUtil.traverseAncestors; +import static com.github.pyknic.rocket.internal.util.ReflectionUtil.traverseMethods; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Collections.*; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +/** + * @author Emil Forslund + * @since 1.0.0 + */ +public final class RocketBuilderImpl> implements RocketBuilder { + + private final Class phasesEnum; + private final List instances; + + public RocketBuilderImpl(Class phasesEnum) { + this.phasesEnum = requireNonNull(phasesEnum); + this.instances = new LinkedList<>(); + } + + @Override + public RocketBuilder with(T instance) { + instances.add(instance); + return this; + } + + @Override + public Rocket build() { + final Map> actions = new EnumMap<>(phasesEnum); + final E[] phases = phasesEnum.getEnumConstants(); + + for (final E phase : phases) { + final List phaseActions = new LinkedList<>(); + + final String phaseName = phase.name(); + final List actionMakers = new LinkedList<>(); + + instances.forEach(inst -> + traverseMethods(inst.getClass()) + .filter(m -> !Modifier.isStatic(m.getModifiers())) + .filter(m -> { + final Execute execute = m.getAnnotation(Execute.class); + return execute != null + && phaseName.equalsIgnoreCase(execute.value()); + }).forEachOrdered(m -> { + final Set> deps = + new HashSet<>(asList(m.getParameterTypes())); + + final String name = format("%s#%s(%s)", + inst.getClass().getSimpleName(), + m.getName(), + Stream.of(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(joining(", ")) + ); + + // Create a node for this action. + actionMakers.add(new ActionMaker(inst.getClass(), + name, deps, () -> { + // Resolve every argument. + final Object[] args = Stream.of(m.getParameterTypes()) + .map(c -> instances.stream() + .filter(c::isInstance).findFirst() + .orElseThrow(() -> new RocketException(format( + "Class '%s' has a method '%s' that has " + + "the @Execute-annotation but one argument" + + " '%s' can't be resolved. Make sure it " + + "is installed in the RocketBuilder.", + inst.getClass().getName(), + m.getName(), + c.getName() + ))) + ).toArray(); + + m.setAccessible(true); + return () -> { + try { + m.invoke(inst, args); + } catch (final IllegalAccessException + | InvocationTargetException ex) { + throw new RocketException( + "Could not invoke annotated method " + + name + ".", ex); + } + }; + })); + }) + ); + + // If there was no actions in this phase, continue. + if (!actionMakers.isEmpty()) { + + // Iterate over the action makers, using the makers as they are + // possible to invoke. + final Set> resolved = new HashSet<>(); + while (!actionMakers.isEmpty()) { + int counter = 0; + + final Iterator it = actionMakers.iterator(); + while (it.hasNext()) { + final ActionMaker am = it.next(); + if (!am.dependencies.stream().allMatch(resolved::contains)) + continue; + + phaseActions.add(am.makeAction.get()); + traverseAncestors(am.clazz).forEach(resolved::add); + + it.remove(); + counter++; + } + + if (counter == 0) { + throw new RocketException( + "Error building " + phaseName + " phase. The " + + "following actions appear to be stuck in an " + + "infinite loop: [\n " + + actionMakers.stream() + .map(am -> am.name) + .collect(joining("\n ")) + + "\n]." + ); + } + } + } + + switch (phaseActions.size()) { + case 0 : actions.put(phase, emptyList()); break; + case 1 : actions.put(phase, singletonList(phaseActions.get(0))); break; + default : actions.put(phase, unmodifiableList(phaseActions)); break; + } + } + + return new RocketImpl<>(actions); + } + + private static final class ActionMaker { + + private final Class clazz; + private final String name; + private final Set> dependencies; + private final Supplier makeAction; + + ActionMaker(Class clazz, + String name, + Set> dependencies, + Supplier makeAction) { + + this.clazz = requireNonNull(clazz); + this.name = requireNonNull(name); + this.dependencies = requireNonNull(dependencies); + this.makeAction = requireNonNull(makeAction); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/pyknic/rocket/internal/RocketImpl.java b/src/main/java/com/github/pyknic/rocket/internal/RocketImpl.java new file mode 100644 index 0000000..8261572 --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/internal/RocketImpl.java @@ -0,0 +1,42 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket.internal; + +import com.github.pyknic.rocket.Rocket; + +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * @author Emil Forslund + * @since 1.0.0 + */ +final class RocketImpl> implements Rocket { + + private final Map> actions; + + RocketImpl(Map> actions) { + this.actions = requireNonNull(actions); + } + + @Override + public void launch(E phase) { + actions.get(phase).forEach(Runnable::run); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/pyknic/rocket/internal/util/ReflectionUtil.java b/src/main/java/com/github/pyknic/rocket/internal/util/ReflectionUtil.java new file mode 100644 index 0000000..51126da --- /dev/null +++ b/src/main/java/com/github/pyknic/rocket/internal/util/ReflectionUtil.java @@ -0,0 +1,100 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket.internal.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.stream.Stream; + +/** + * Some common utility methods for analyzing classes with reflection. + * + * @author Emil Forslund + * @since 1.0.0 + */ +public final class ReflectionUtil { + + /** + * Returns a stream of all the member fields for the specified class, + * including inherited fields from any ancestors. This includes public, + * private, protected and package private fields. + * + * @param clazz the class to traverse + * @return stream of fields + */ + public static Stream traverseFields(Class clazz) { + final Class parent = clazz.getSuperclass(); + final Stream inherited; + + if (parent != null) { + inherited = traverseFields(parent); + } else { + inherited = Stream.empty(); + } + + return Stream.concat(inherited, Stream.of(clazz.getDeclaredFields())); + } + + /** + * Returns a stream of all methods in the specified class, including + * inherited ones. + * + * @param clazz the class to traverse + * @return stream of methods + */ + public static Stream traverseMethods(Class clazz) { + return traverseAncestors(clazz) + .map(Class::getDeclaredMethods) + .flatMap(Stream::of); + } + + /** + * Returns a stream of all the classes upwards in the inheritance tree of + * the specified class, including the class specified as the first element + * and {@code java.lang.Object} as the last one. + * + * @param clazz the first class in the tree + * @return stream of ancestors (including {@code clazz}) + */ + public static Stream> traverseAncestors(Class clazz) { + final Class[] interfaces = clazz.getInterfaces(); + if (clazz.getSuperclass() == null) { + if (interfaces.length == 0) { + return Stream.of(clazz); + } else { + return Stream.concat( + Stream.of(clazz), + Stream.of(clazz.getInterfaces()) + .flatMap(ReflectionUtil::traverseAncestors) + ).distinct(); + } + } else { + return Stream.concat( + Stream.of(clazz), + Stream.concat( + Stream.of(clazz.getSuperclass()), + Stream.of(clazz.getInterfaces()) + ).flatMap(ReflectionUtil::traverseAncestors) + ).distinct(); + } + } + + /** + * Should never be invoked. + */ + private ReflectionUtil() {} +} \ No newline at end of file diff --git a/src/test/java/com/github/pyknic/rocket/RocketTest.java b/src/test/java/com/github/pyknic/rocket/RocketTest.java new file mode 100644 index 0000000..9068e57 --- /dev/null +++ b/src/test/java/com/github/pyknic/rocket/RocketTest.java @@ -0,0 +1,141 @@ +/** + * + * Copyright (c) 2017, Emil Forslund. All Rights Reserved. + * + * 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 com.github.pyknic.rocket; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.github.pyknic.rocket.RocketTest.Phase.DESTROY; +import static com.github.pyknic.rocket.RocketTest.Phase.INIT; +import static com.github.pyknic.rocket.RocketTest.Phase.UPDATE; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Emil Forslund + * @since 1.0.0 + */ +class RocketTest { + + enum Phase { + INIT, + UPDATE, + DESTROY + } + + boolean firstInitiated; + boolean secondInitiated; + boolean thirdInitiated; + + boolean firstUpdated; + boolean secondUpdated; + boolean thirdUpdated; + + boolean firstDestroyed; + boolean secondDestroyed; + boolean thirdDestroyed; + + @BeforeEach + void reset() { + firstInitiated = false; + secondInitiated = false; + thirdInitiated = false; + + firstUpdated = false; + secondUpdated = false; + thirdUpdated = false; + + firstDestroyed = false; + secondDestroyed = false; + thirdDestroyed = false; + } + + @Test + void launch() { + + class First { + @Execute("init") void init() {firstInitiated = true;} + @Execute("update") void update() {firstUpdated = true;} + @Execute("destroy") void destroy() {firstDestroyed = true;} + } + + class Second { + @Execute("init") void init() {secondInitiated = true;} + @Execute("update") void update(First first) {secondUpdated = true;} + @Execute("destroy") void destroy() {secondDestroyed = true;} + } + + class Third { + @Execute("init") void init(First first) {thirdInitiated = true;} + @Execute("update") void update(First first, Second second) {thirdUpdated = true;} + @Execute("destroy") void destroy(Second second) {thirdDestroyed = true;} + } + + final Rocket rocket = Rocket.builder(Phase.class) + .with(new First()) + .with(new Second()) + .with(new Third()) + .build(); + + assertFalse(firstInitiated); + assertFalse(firstUpdated); + assertFalse(firstDestroyed); + assertFalse(secondInitiated); + assertFalse(secondUpdated); + assertFalse(secondDestroyed); + assertFalse(thirdInitiated); + assertFalse(thirdUpdated); + assertFalse(thirdDestroyed); + + rocket.launch(INIT); + + assertTrue(firstInitiated); + assertFalse(firstUpdated); + assertFalse(firstDestroyed); + assertTrue(secondInitiated); + assertFalse(secondUpdated); + assertFalse(secondDestroyed); + assertTrue(thirdInitiated); + assertFalse(thirdUpdated); + assertFalse(thirdDestroyed); + + rocket.launch(UPDATE); + + assertTrue(firstInitiated); + assertTrue(firstUpdated); + assertFalse(firstDestroyed); + assertTrue(secondInitiated); + assertTrue(secondUpdated); + assertFalse(secondDestroyed); + assertTrue(thirdInitiated); + assertTrue(thirdUpdated); + assertFalse(thirdDestroyed); + + rocket.launch(DESTROY); + + assertTrue(firstInitiated); + assertTrue(firstUpdated); + assertTrue(firstDestroyed); + assertTrue(secondInitiated); + assertTrue(secondUpdated); + assertTrue(secondDestroyed); + assertTrue(thirdInitiated); + assertTrue(thirdUpdated); + assertTrue(thirdDestroyed); + } + +} \ No newline at end of file