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

Compare exported vs deployed .ocl files for review + readme.

parent 748b76ca
Loading
Loading
Loading
Loading
+58 −4
Changes for plugins/org.etsi.mts.tdl.constraints.helper/README.md: 58 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -10,7 +10,8 @@ The implementation of the tool makes use of the Apache POI and related libraries

## Command line

    ConstraintExporter --format evl|ocl|ecore --in <file> [--ecore <file>] [--exclude Class::Name,...]
    ConstraintExporter --format evl|ocl|ecore --in <file> [--ecore <file>] [--severities <file>]
                       [--interpreted-ocl <file>] [--compare-ocl <file>] [--exclude Class::Name,...]
                       [--import <uri>,...] [--generated-package <name>]

Classpath: the compiled classes, `lib/*` and the plug-ins of an Eclipse Modeling installation (for EMF).
@@ -20,14 +21,67 @@ Classpath: the compiled classes, `lib/*` and the plug-ins of an Eclipse Modeling
- `--format ocl` (and `evl`) writes `resources/generated/tdl-generated-<title>.ocl`.
- `--format ecore` additionally writes the invariants into the metamodel given with `--ecore`, in the
  form the OCLinEcore code generator compiles: per class the `constraints` list of the Ecore annotation,
  and per invariant the details `<Name>` (the expression) and `<Name>$message` (the description of the
  constraint followed by `self.toString()`). `error(...)` and `warning(...)` wrappers around an
  expression are dropped. The file is written with the EMF API,
  and per invariant one detail `<Name>` of the Pivot annotation holding the tuple-valued invariant of
  Eclipse OCL:

      Tuple { message : String = '<description of the constraint>' + self.toString(), severity : Integer = -1, status : Boolean = <expression>
      }.status

  The expression is copied as is, a line break before the closing brace is there because an
  expression may end in a `--` comment.

  The `severity` part is only written for invariants listed in the `--severities` file. The deprecated
  `<Name>$message` detail, which Eclipse OCL converts to this form on load, is removed when found.
  The file is written with the EMF API,
  so it keeps the formatting of the Ecore editor. The invariants that are no longer in the input are removed
   from the classes that are in the input. Operation bodies are not touched. 
   A report lists what was added, updated, left unchanged and
  removed, the classes it did not find, and the invariants that use `allInstances()`.
- `--severities` names a file of lines `Class::Name = error|warning|info` (`#` starts a comment)
  (`model/constraint-severities.properties` in the model plug-in), written as the `severity` part
  (-1, 1, 0).
- `--interpreted-ocl` writes the Complete OCL export (from `--format ocl` or `ecore`) once more, with the
  invariants that have a configured severity in the same tuple form: what the deployed document of the
  interpreted validator should contain. Conventionally `model/interpreted/<document>.ocl` in the model
  plug-in. It is not loaded, acts as the reference for the review.
- `--compare-ocl` compares the export (the `--interpreted-ocl` one if given) with a deployed Complete OCL
  document, per invariant and ignoring whitespace: expressions that differ, invariants missing on either
  side. Nothing is written.
- `--exclude` keeps the named invariants out of the metamodel (they stay in the OCL output), for
  invariants the code generator cannot compile.

Writing the invariants into a metamodel makes the EMF generator produce a validator for it.

## Workflow

The specification is canonical - each part is exported on its own (one run per document):

| Part | Document | Package options | Metamodel | Deployed OCL |
|---|---|---|---|---|
| 1 | `es_20311901…docx` | (default `tdl`) | `tdl.ecore` | `tdl-constraints.ocl` |
| 4 | `es_20311904…docx` | `--generated-package structuredobjectives --import http://www.etsi.org/spec/TDL/1.3.1/structured` | `structured.ecore` | `tdl-structured-constraints.ocl` |
| 7 | `es_20311907…docx` | `--generated-package extendedconfigurations --import http://www.etsi.org/spec/TDL/1.3.1/configurations` | `configurations.ecore` | `tdl-configurations-constraints.ocl` |

Metamodel (compiled constraints, the default), from the plug-in folder:

    ConstraintExporter --format ecore --in <part 1 .docx>
        --ecore ../org.etsi.mts.tdl.model/model/tdl.ecore
        --severities ../org.etsi.mts.tdl.model/model/constraint-severities.properties

Review of the deployed Complete OCL documents (interpreted constraints, being deprecated; they are
maintained by hand):

    ConstraintExporter --format ocl --in <part 1 .docx>
        --severities ../org.etsi.mts.tdl.model/model/constraint-severities.properties
        --interpreted-ocl ../org.etsi.mts.tdl.model/model/interpreted/tdl-constraints.ocl
        --compare-ocl ../org.etsi.mts.tdl.model/model/tdl-constraints.ocl

then correct the specification or the deployed document (porting from the export) and run again until
the comparison reports no differences.

Upgrade idea: apply the severities at runtime instead of at export, in the interpreted validator
(`Validator` in the model plug-in: wrap the `DiagnosticChain` in `validatePivot` and set the severity of
the diagnostics of configured constraints, recognised by their description) and, for the compiled
invariants, through the OCL executor's severity hook or the Xtext diagnostic converter. One mechanism
for both implementations, severities changeable without a re-export, and nothing to port by hand into
the deployed documents; the cost is matching diagnostics back to constraints by message.
+58 −4
Changes for plugins/org.etsi.mts.tdl.constraints.helper/src/org/etsi/mts/tdl/constraints/helper/ConstraintExporter.java: 58 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ public class ConstraintExporter {
		java.util.Set<String> excluded = new java.util.HashSet<>();
		String severities = null;
		String interpretedOcl = null;
		String compareOcl = null;

		for (int i = 0; i < args.length; i++) {
			if (args[i].equalsIgnoreCase("--format") && args.length > i + 1) {
@@ -67,19 +68,25 @@ public class ConstraintExporter {
				severities = args[i + 1];
			if (args[i].equalsIgnoreCase("--interpreted-ocl") && args.length > i + 1)
				interpretedOcl = args[i + 1];
			if (args[i].equalsIgnoreCase("--compare-ocl") && args.length > i + 1)
				compareOcl = args[i + 1];
		}

		ConstraintExporter exporter = new ConstraintExporter();
		exporter.format = format;
		java.util.List<ConstraintRecord> constraints;
		// the Complete OCL text of the constraints: the input itself, or the export of the specification
		Path oclText;
		if (filename.endsWith(".ocl")) {
			// A Complete OCL document as input, only the ecore target makes sense.
			constraints = CompleteOclReader.read(Path.of(filename));
			oclText = Path.of(filename);
			System.out.println("Read " + constraints.size() + " invariants from " + filename);
		} else {
			Document doc = DocumentHandler.loadDocument(new File(filename).getAbsolutePath());
			String title = doc.getTitle().replaceAll(" ", "-");
			String targetPath = "resources/generated/tdl-generated-" + title + (format == OutputFormat.EVL ? ".evl" : ".ocl");
			oclText = format == OutputFormat.EVL ? null : Path.of(targetPath);
			exporter.exportConstraints(doc, generatedPackage, extraImports, targetPath);
			exporter.exportModel(doc, generatedPackage, extraImports, "resources/generated/tdl-generated-" + title + ".txt", true);
			constraints = exporter.getRecords();
@@ -91,10 +98,10 @@ public class ConstraintExporter {
				exporter.exportGrammar(grammarDoc, generatedPackage, extraImports, "resources/generated/tdl-generated-" + title + "-grammar.txt", true);
			}
		}
		java.util.Map<String, Integer> severityOf = severities == null ? java.util.Collections.emptyMap() : readSeverities(Path.of(severities));
		if (format == OutputFormat.ECORE) {
			if (ecore == null)
				throw new IllegalArgumentException("--format ecore requires --ecore <path to .ecore>");
			java.util.Map<String, Integer> severityOf = severities == null ? java.util.Collections.emptyMap() : readSeverities(Path.of(severities));
			java.util.Set<String> unused = new java.util.TreeSet<>(severityOf.keySet());
			// Invariants the OCL code generator cannot compile (Class::Name) are kept in the Complete OCL text only (temporary)
			java.util.List<ConstraintRecord> compilable = new java.util.ArrayList<>();
@@ -113,13 +120,60 @@ public class ConstraintExporter {
			EcoreConstraintEmitter.Report report = EcoreConstraintEmitter.apply(Path.of(ecore), compilable);
			System.out.println("Wrote invariants into " + ecore);
			System.out.print(report);
		}
		// Review of a deployed document: the export (with the severities) is written next to it and compared with it.
		// Either the specification or the deployed document is corrected until they agree.
		if ((interpretedOcl != null || compareOcl != null) && oclText == null)
			throw new IllegalArgumentException("--interpreted-ocl and --compare-ocl need a Complete OCL export (--format ocl or ecore)");
		Path reference = oclText;
		if (interpretedOcl != null) {
				if (!filename.endsWith(".ocl"))
					throw new IllegalArgumentException("--interpreted-ocl requires a Complete OCL document as --in");
				java.util.List<String> rewritten = InterpretedOclWriter.write(Path.of(filename), Path.of(interpretedOcl), severityOf);
			java.util.List<String> rewritten = InterpretedOclWriter.write(oclText, Path.of(interpretedOcl), severityOf);
			reference = Path.of(interpretedOcl);
			System.out.println("Wrote the interpreted document " + interpretedOcl + (rewritten.isEmpty() ? "" : ", with severities: " + String.join(", ", rewritten)));
		}
		if (compareOcl != null) {
			System.out.print(compare(CompleteOclReader.read(reference), Path.of(compareOcl)));
		}
	}

	/**
	 * The differences between the exported constraints and a deployed Complete OCL document, per invariant:
	 * expression (an invariant with a severity: its tuple) or description changed, whitespace ignored, and
	 * invariants missing on either side. A report for porting changes by hand.
	 */
	static String compare(java.util.List<ConstraintRecord> exported, Path deployedFile) throws IOException {
		java.util.Map<String, ConstraintRecord> deployed = new java.util.LinkedHashMap<>();
		for (ConstraintRecord r : CompleteOclReader.read(deployedFile)) {
			deployed.put(r.toString(), r);
		}
		java.util.List<String> lines = new java.util.ArrayList<>();
		java.util.Set<String> seen = new java.util.HashSet<>();
		for (ConstraintRecord r : exported) {
			String id = r.toString();
			seen.add(id);
			ConstraintRecord d = deployed.get(id);
			if (d == null) {
				lines.add("  + " + id + ": not in " + deployedFile.getFileName());
				continue;
			}
			if (!normalised(r.expression).equals(normalised(d.expression))) {
				lines.add("  ~ " + id + ": expression differs");
			}
			// in the tuple form the description is part of the expression, the header has none
			if (!r.description.isEmpty() && !d.description.isEmpty() && !normalised(r.description).equals(normalised(d.description))) {
				lines.add("  ~ " + id + ": description differs");
			}
		}
		for (String id : deployed.keySet()) {
			if (!seen.contains(id)) {
				lines.add("  - " + id + ": only in " + deployedFile.getFileName());
			}
		}
		return "Compared with " + deployedFile + ": " + (lines.isEmpty() ? "no differences\n" : lines.size() + " differences\n" + String.join("\n", lines) + "\n");
	}

	private static String normalised(String s) {
		return s.trim().replaceAll("\\s+", " ");
	}

	/**