Commit 94579594 authored by Martti Käärik's avatar Martti Käärik
Browse files

Code generator vs runtime API contract and automated test.

parent eb65babe
Loading
Loading
Loading
Loading
+110 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.codegen;

/**
 * Names of the runtime members referenced <b>by name</b> from generated test
 * code, i.e. the dependency of this generator on the runtime that the compiler
 * cannot check. Every member listed here carries the {@code GeneratedCodeApi}
 * annotation in the runtime; keep the two in sync when renaming.
 */
final class RuntimeApi {

	private RuntimeApi() {
	}

	// ---- TestControl (the "tc" field of generated tests)

	static final String SYSTEM_ADAPTER = "systemAdapter";
	static final String VALIDATOR = "validator";
	static final String REPORTER = "reporter";
	static final String FUNCTIONS = "functions";

	static final String GET_TESTER_COMPONENT = "getTesterComponent";
	static final String VALUE = "value";
	static final String TERMINATE = "terminate";
	static final String STOP = "stop";
	static final String TIMEOUT = "timeout";
	static final String TIME_CONSTRAINT_DEADLINE = "timeConstraintDeadline";
	static final String TIME = "time";
	static final String SLEEP = "sleep";
	static final String SHUTDOWN = "shutdown";
	static final String RESUME_RECEIVING = "resumeReceiving";
	static final String REMOVE_EXCEPTIONAL_BEHAVIOUR = "removeExceptionalBehaviour";
	static final String NO_INPUT = "noInput";
	static final String NEXT = "next";
	static final String GET_GATE_REFERENCE = "getGateReference";
	static final String GET_CONNECTION = "getConnection";
	static final String ENABLE_EXCEPTIONAL_BEHAVIOURS = "enableExceptionalBehaviours";
	static final String DISABLE_ACTIVE_EXCEPTIONAL_BEHAVIOURS = "disableActiveExceptionalBehaviours";
	static final String CONFIGURE = "configure";
	static final String CLEANUP_BATCH = "cleanupBatch";
	static final String CHECK_TIME_CONSTRAINT = "checkTimeConstraint";
	static final String CALL = "call";
	static final String BREAK_BLOCK = "breakBlock";
	static final String AWAIT_TIME_CONSTRAINT = "awaitTimeConstraint";
	static final String ADD_EXCEPTIONAL_BEHAVIOUR = "addExceptionalBehaviour";
	static final String EXECUTE_ALTERNATIVES_AND_EXCEPTIONALS = "executeAlternativesAndExceptionals";
	static final String RECEIVE = "receive";
	static final String TRIGGER = "trigger";
	static final String IS_TRUE = "isTrue";
	static final String INTEGER = "integer";
	static final String MILLIS = "millis";
	static final String GET_EXCEPTIONAL_BEHAVIOUR = "getExceptionalBehaviour";

	// ---- Other runtime classes (owner#member)
	static final String EXECUTE = "execute"; // ExecutionCallable#execute
	static final String GET_TIMESTAMP = "getTimestamp"; // ExecutionResult#getTimestamp
	static final String DATA = "data"; // InteractionResult#data
	static final String ALTERNATIVES = "alternatives"; // BatchedFutures#alternatives
	static final String EXCEPTIONALS = "exceptionals"; // BatchedFutures#exceptionals
	static final String ADD_TEST = "addTest"; // ComponentExecutor#addTest
	static final String BEHAVIOUR = "behaviour"; // ExceptionalBehaviour#behaviour
	static final String IS_INTERRUPT = "isInterrupt"; // ExceptionalBehaviour#isInterrupt
	static final String SET_CALLABLE = "setCallable"; // ExceptionalBehaviour#setCallable
	static final String GET_VERDICT = "getVerdict"; // StopExceptionImpl#getVerdict
	static final String GET_BLOCK_NAME = "getBlockName"; // BreakException#getBlockName
	static final String SET_TEMPLATE = "setTemplate"; // ValueImpl#setTemplate
	static final String SET_IS_COLLECTION = "setIsCollection"; // ValueImpl#setIsCollection
	static final String SET_IS_STRUCTURE = "setIsStructure"; // ValueImpl#setIsStructure
	static final String SET_MAPPING = "setMapping"; // ValueImpl#setMapping
	static final String SET_VALUE = "setValue"; // ValueImpl#setValue
	static final String SET_PARAMETER = "setParameter"; // ValueImpl#setParameter
	static final String ADD_ITEM = "addItem"; // ValueImpl#addItem
	static final String SET_NAME = "setName"; // ValueImpl#setName
	static final String ADD_ANNOTATION = "addAnnotation"; // ValueImpl#addAnnotation
	static final String AS_DATA = "asData"; // ValueImpl#asData
	static final String SET_ITEM_TYPE = "setItemType"; // TypeImpl#setItemType
	static final String SET_PARENT = "setParent"; // MappingImpl#setParent
	static final String SET_IS_RESOURCE = "setIsResource"; // MappingImpl#setIsResource
	static final String SET_IS_PARAMETER = "setIsParameter"; // MappingImpl#setIsParameter
	static final String SET_RESOURCE = "setResource"; // MappingImpl#setResource
	static final String NONE = "NONE"; // TemplateImpl#NONE
	static final String MARK = "mark"; // TemplateImpl#mark
	static final String MEMBER = "member"; // TemplateImpl#member
	static final String ITEM = "item"; // TemplateImpl#item
	static final String SET_UNASSIGNED_MEMBER_TREATMENT = "setUnassignedMemberTreatment"; // TemplateImpl#setUnassignedMemberTreatment
	static final String EVALUATE = "evaluate"; // Constraint#evaluate
	static final String HOLDS = "holds"; // Evaluation#holds
	static final String OPERANDS = "operands"; // Evaluation#operands
	static final String OF = "of"; // BehaviourProperties#of
	static final String PROPERTY_SOURCE = "SOURCE"; // BehaviourProperties#SOURCE
	static final String PROPERTY_RESULT = "RESULT"; // BehaviourProperties#RESULT
	static final String PROPERTY_OPERATOR = "OPERATOR"; // BehaviourProperties#OPERATOR
	static final String PROPERTY_OPERANDS = "OPERANDS"; // BehaviourProperties#OPERANDS
	static final String PROPERTY_VALUE = "VALUE"; // BehaviourProperties#VALUE
	static final String PROPERTY_TIME_CONSTRAINT_RESULT = "TIME_CONSTRAINT_RESULT"; // BehaviourProperties#TIME_CONSTRAINT_RESULT
	static final String PROPERTY_TIME_CONSTRAINT_OPERATOR = "TIME_CONSTRAINT_OPERATOR"; // BehaviourProperties#TIME_CONSTRAINT_OPERATOR
	static final String PROPERTY_TIME_CONSTRAINT_OPERANDS = "TIME_CONSTRAINT_OPERANDS"; // BehaviourProperties#TIME_CONSTRAINT_OPERANDS
	static final String TIMESTAMP = "timestamp"; // TimeLabel#timestamp
	static final String LAST = "last"; // TimeLabel#last
	static final String FIRST = "first"; // TimeLabel#first
	static final String PREVIOUS = "previous"; // TimeLabel#previous
	static final String FIRST_OR = "firstOr"; // TimeLabel#firstOr
	static final String NOW_MILLIS = "nowMillis"; // TimeLabel#nowMillis
	static final String TIMER_START = "start"; // Timer#start
	static final String TIMER_STOP = "stop"; // Timer#stop
	static final String FAIL = "fail"; // VerdictImpl#fail
	static final String PASS = "pass"; // VerdictImpl#pass (verdict instances are referenced by their TDL names)
	static final String INCONCLUSIVE = "inconclusive"; // VerdictImpl#inconclusive
	static final String TO_MILLIS = "toMillis"; // TimeUnit#toMillis

}
+12 −1
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@
	<version>1.0.0</version>
	<build>
		<sourceDirectory>src</sourceDirectory>
		<testSourceDirectory>test</testSourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
@@ -15,6 +16,10 @@
					<release>11</release>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>3.2.5</version>
			</plugin>
			<plugin>
				<artifactId>maven-assembly-plugin</artifactId>
				<configuration>
@@ -65,7 +70,13 @@
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<version>5.9.2</version>
			<version>5.10.0</version>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-engine</artifactId>
			<version>5.10.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.assertj</groupId>
+21 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.rt.core;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Marks runtime members that generated test code references <b>by name</b>
 * (the code generator emits them as strings, so the compiler cannot enforce the
 * dependency). Renaming or changing the signature of an annotated member
 * requires the corresponding change in the code generator, which lists these
 * names in its {@code RuntimeApi} class. Members without this annotation are
 * free to change as long as the runtime compiles.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE })
public @interface GeneratedCodeApi {
}
+121 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.rt.core;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

/**
 * Cross-checks the contract between the code generator and the runtime: every
 * runtime member the generator references by name (listed in the generator's
 * {@code RuntimeApi} class) must exist and carry {@link GeneratedCodeApi}, and
 * every annotated method or field must be listed there. The generator source is
 * read as a file, as the two modules share no compile-time dependency.
 */
class GeneratedCodeApiTest {

	/**
	 * Location of the generator's RuntimeApi source, relative to this module;
	 * overridable with the system property {@code tdl.runtimeApi}.
	 */
	private static final Path RUNTIME_API = Paths.get(System.getProperty("tdl.runtimeApi",
			"../org.etsi.mts.tdl.execution.java.codegen/src/org/etsi/mts/tdl/execution/java/codegen/RuntimeApi.java"));

	private static final Path CORE_SOURCES = Paths.get("src/org/etsi/mts/tdl/execution/java/rt/core");

	private static final Pattern CONSTANT = Pattern
			.compile("static final String (\\w+) = \"(\\w+)\";(?:\\s*//\\s*(\\w+)#(\\w+))?");

	/** owner simple name -> referenced member names */
	private static Map<String, Set<String>> referencedMembers() throws IOException {
		Map<String, Set<String>> refs = new HashMap<>();
		for (String line : Files.readAllLines(RUNTIME_API)) {
			Matcher m = CONSTANT.matcher(line);
			if (!m.find())
				continue;
			String owner = m.group(3) != null ? m.group(3) : "TestControl";
			refs.computeIfAbsent(owner, k -> new HashSet<>()).add(m.group(2));
		}
		assertTrue(!refs.isEmpty(), "No constants found in " + RUNTIME_API.toAbsolutePath());
		return refs;
	}

	/** All classes of the runtime core package, nested classes included. */
	private static List<Class<?>> coreClasses() throws IOException, ClassNotFoundException {
		List<Class<?>> classes = new ArrayList<>();
		try (Stream<Path> files = Files.list(CORE_SOURCES)) {
			for (Path f : files.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList())) {
				String name = f.getFileName().toString().replace(".java", "");
				collect(Class.forName(GeneratedCodeApi.class.getPackageName() + "." + name), classes);
			}
		}
		return classes;
	}

	private static void collect(Class<?> c, List<Class<?>> into) {
		into.add(c);
		for (Class<?> nested : c.getDeclaredClasses())
			collect(nested, into);
	}

	private static Set<String> annotatedMembers(Class<?> c) {
		Set<String> names = new HashSet<>();
		for (Method m : c.getDeclaredMethods())
			if (m.isAnnotationPresent(GeneratedCodeApi.class))
				names.add(m.getName());
		for (Field f : c.getDeclaredFields())
			if (f.isAnnotationPresent(GeneratedCodeApi.class))
				names.add(f.getName());
		return names;
	}

	@Test
	void everyGeneratorReferenceIsAnAnnotatedRuntimeMember() throws Exception {
		Map<String, Set<String>> refs = referencedMembers();
		Map<String, Class<?>> bySimpleName = new HashMap<>();
		for (Class<?> c : coreClasses())
			bySimpleName.put(c.getSimpleName(), c);

		List<String> problems = new ArrayList<>();
		refs.forEach((owner, members) -> {
			Class<?> c = bySimpleName.get(owner);
			if (c == null) {
				problems.add(owner + ": no such runtime class");
				return;
			}
			Set<String> annotated = annotatedMembers(c);
			for (String member : members)
				if (!annotated.contains(member))
					problems.add(owner + "#" + member + ": not an @GeneratedCodeApi member");
		});
		assertTrue(problems.isEmpty(), "RuntimeApi references without annotated runtime member:\n" + String.join("\n", problems));
	}

	@Test
	void everyAnnotatedMemberIsReferencedByTheGenerator() throws Exception {
		Set<String> referenced = referencedMembers().values().stream().flatMap(Set::stream).collect(Collectors.toSet());
		List<String> problems = new ArrayList<>();
		for (Class<?> c : coreClasses())
			for (String member : annotatedMembers(c))
				if (!referenced.contains(member))
					problems.add(c.getSimpleName() + "#" + member);
		assertTrue(problems.isEmpty(), "@GeneratedCodeApi members not listed in RuntimeApi:\n" + String.join("\n", problems));
	}

}