ExecutionTreeSnapshot.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Immutable framework-neutral snapshot of one completed execution tree.
*
* <p>The model deliberately contains no JUnit or Eclipse launch types. Adapters
* assign stable semantic identities to containers and tests while retaining
* optional display names and attributes for diagnostics. Child order and
* duplicate occurrences are preserved, so ordering and multiplicity remain
* observable migration contracts.</p>
*/
public record ExecutionTreeSnapshot(List<Node> roots, boolean successful) {
/** Creates a defensive immutable snapshot. */
public ExecutionTreeSnapshot {
roots= immutableNodes(roots);
}
/** Returns whether no execution element was captured. */
public boolean isEmpty() {
return roots.isEmpty();
}
/** Stable execution element kinds. */
public enum NodeKind {
CONTAINER,
TEST,
OTHER
}
/**
* One immutable execution element.
*
* @param kind semantic element kind
* @param identity stable comparison identity supplied by the adapter
* @param displayName optional framework display name, normalized to an empty string
* @param result optional result token, normalized to an empty string
* @param attributes immutable adapter-defined diagnostic attributes
* @param children ordered child occurrences
*/
public record Node(NodeKind kind, String identity, String displayName, String result,
Map<String, String> attributes, List<Node> children) {
/** Creates a defensive immutable node. */
public Node {
kind= Objects.requireNonNull(kind);
identity= requireText(identity, "Execution node identity"); //$NON-NLS-1$
displayName= displayName == null ? "" : displayName; //$NON-NLS-1$
result= result == null ? "" : result; //$NON-NLS-1$
attributes= immutableAttributes(attributes);
children= immutableNodes(children);
if (kind == NodeKind.TEST && !children.isEmpty()) {
throw new IllegalArgumentException("Execution test nodes cannot contain children"); //$NON-NLS-1$
}
}
/** Creates a container node without attributes. */
public static Node container(String identity, String displayName, String result, List<Node> children) {
return new Node(NodeKind.CONTAINER, identity, displayName, result, Map.of(), children);
}
/** Creates a test node without attributes. */
public static Node test(String identity, String displayName, String result) {
return new Node(NodeKind.TEST, identity, displayName, result, Map.of(), List.of());
}
/** Creates an adapter-specific node. */
public static Node other(String identity, String displayName, String result,
Map<String, String> attributes, List<Node> children) {
return new Node(NodeKind.OTHER, identity, displayName, result, attributes, children);
}
}
private static List<Node> immutableNodes(List<Node> nodes) {
if (nodes == null || nodes.isEmpty()) {
return List.of();
}
return nodes.stream().map(Objects::requireNonNull).toList();
}
private static Map<String, String> immutableAttributes(Map<String, String> attributes) {
if (attributes == null || attributes.isEmpty()) {
return Map.of();
}
Map<String, String> copy= new LinkedHashMap<>();
attributes.forEach((key, value) -> copy.put(requireText(key, "Execution attribute name"), //$NON-NLS-1$
Objects.requireNonNull(value)));
return Map.copyOf(copy);
}
private static String requireText(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " must not be blank"); //$NON-NLS-1$
}
return value;
}
}