Skip to content

Package: EcoreEditorPerformance_PTest$Editor$1

EcoreEditorPerformance_PTest$Editor$1

nameinstructionbranchcomplexitylinemethod
updateViewModelProperties(VViewModelProperties)
M: 0 C: 7
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
{...}
M: 0 C: 6
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%

Coverage

1: /*******************************************************************************
2: * Copyright (c) 2018 Christian W. Damus and others.
3: *
4: * All rights reserved. This program and the accompanying materials
5: * are made available under the terms of the Eclipse Public License 2.0
6: * which accompanies this distribution, and is available at
7: * https://www.eclipse.org/legal/epl-2.0/
8: *
9: * SPDX-License-Identifier: EPL-2.0
10: *
11: * Contributors:
12: * Christian W. Damus - initial API and implementation
13: ******************************************************************************/
14: package org.eclipse.emfforms.spi.editor;
15:
16: import static org.hamcrest.CoreMatchers.is;
17: import static org.hamcrest.MatcherAssert.assertThat;
18: import static org.junit.Assert.fail;
19:
20: import java.io.IOException;
21: import java.lang.annotation.ElementType;
22: import java.lang.annotation.Retention;
23: import java.lang.annotation.RetentionPolicy;
24: import java.lang.annotation.Target;
25: import java.util.ArrayList;
26: import java.util.Collections;
27: import java.util.List;
28: import java.util.Random;
29: import java.util.function.Consumer;
30:
31: import org.eclipse.core.resources.IFile;
32: import org.eclipse.core.resources.IProject;
33: import org.eclipse.core.resources.IResource;
34: import org.eclipse.core.resources.IWorkspaceRunnable;
35: import org.eclipse.core.resources.ResourcesPlugin;
36: import org.eclipse.core.runtime.CoreException;
37: import org.eclipse.core.runtime.IProgressMonitor;
38: import org.eclipse.core.runtime.IStatus;
39: import org.eclipse.core.runtime.NullProgressMonitor;
40: import org.eclipse.core.runtime.Platform;
41: import org.eclipse.core.runtime.Status;
42: import org.eclipse.core.runtime.SubMonitor;
43: import org.eclipse.emf.common.command.Command;
44: import org.eclipse.emf.common.util.BasicDiagnostic;
45: import org.eclipse.emf.common.util.Diagnostic;
46: import org.eclipse.emf.common.util.URI;
47: import org.eclipse.emf.ecore.EClass;
48: import org.eclipse.emf.ecore.EClassifier;
49: import org.eclipse.emf.ecore.EEnum;
50: import org.eclipse.emf.ecore.EEnumLiteral;
51: import org.eclipse.emf.ecore.ENamedElement;
52: import org.eclipse.emf.ecore.EObject;
53: import org.eclipse.emf.ecore.EPackage;
54: import org.eclipse.emf.ecore.EcoreFactory;
55: import org.eclipse.emf.ecore.EcorePackage;
56: import org.eclipse.emf.ecore.resource.Resource;
57: import org.eclipse.emf.ecore.resource.ResourceSet;
58: import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
59: import org.eclipse.emf.ecore.util.EcoreUtil;
60: import org.eclipse.emf.ecp.test.common.PerformanceClock;
61: import org.eclipse.emf.ecp.view.spi.common.callback.ViewModelPropertiesUpdateCallback;
62: import org.eclipse.emf.ecp.view.spi.model.VViewModelProperties;
63: import org.eclipse.emf.ecp.view.spi.validation.ValidationProvider;
64: import org.eclipse.emf.edit.command.AddCommand;
65: import org.eclipse.emf.edit.domain.EditingDomain;
66: import org.eclipse.emfforms.internal.editor.ecore.EcoreEditor;
67: import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeMasterDetailComposite;
68: import org.eclipse.emfforms.spi.swt.treemasterdetail.util.CreateElementCallback;
69: import org.eclipse.swt.widgets.Composite;
70: import org.eclipse.swt.widgets.Display;
71: import org.eclipse.ui.IEditorPart;
72: import org.eclipse.ui.IViewPart;
73: import org.eclipse.ui.IWorkbench;
74: import org.eclipse.ui.IWorkbenchPage;
75: import org.eclipse.ui.IWorkbenchWindow;
76: import org.eclipse.ui.PartInitException;
77: import org.eclipse.ui.PlatformUI;
78: import org.eclipse.ui.ide.IDE;
79: import org.junit.After;
80: import org.junit.BeforeClass;
81: import org.junit.Rule;
82: import org.junit.Test;
83: import org.junit.rules.TestWatcher;
84: import org.junit.runner.Description;
85: import org.osgi.framework.Bundle;
86: import org.osgi.framework.FrameworkUtil;
87:
88: /**
89: * Performance tests for the <em>EMF Forms GenericEditor</em>, using the Ecore editor
90: * as test subject.
91: *
92: * @author Christian W. Damus
93: */
94: @SuppressWarnings({ "nls", "restriction" })
95: public class EcoreEditorPerformance_PTest {
96:
97:         private static final char[] NAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
98:                 .toCharArray();
99:         private static final char[] NAME_CHARS_NO_DIGITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
100:                 .toCharArray();
101:
102:         private static final String SMALL_FILE_NAME = "SmallScale.ecore";
103:         private static final String LARGE_FILE_NAME = "LargeScale.ecore";
104:
105:         private static final int SMALL_SCALE = 50;
106:         private static final int LARGE_SCALE = 3000;
107:
108:         private static final int ITERATIONS = 10;
109:
110:         // Something about the Linux environment seems to add to the cost (window manager?)
111:         // even in a virtual machine on a Mac host. So, considering that the large model has
112:         // 60 times the number of elements as the small model, a worst-case factor of 30
113:         // could be considered generous for an expected linear scaling of performance (even
114:         // though experiments on a local Mac are shown to be much better than this)
115:         private static final double WORST_CASE_MULTIPLIER = Platform.WS_GTK.equals(Platform.getWS())
116:                 ? 30.0
117:                 : 10.0;
118:
119:         private static Random random = new Random(System.currentTimeMillis());
120:
121:         @Rule
122:         public final ProjectRule project = new ProjectRule();
123:
124:         /**
125:          * Initializes me.
126:          */
127:         public EcoreEditorPerformance_PTest() {
128:                 super();
129:         }
130:
131:         /**
132:          * Regression test for <a href="http://eclip.se/533568">bug 533568</a> in which the
133:          * {@link GenericEditor} leaks an instance of the {@link TreeMasterDetailComposite} and
134:          * its attendant "limbo" shell after the editor is closed, until workbench shutdown.
135:          */
136:         @Test
137:         @TestResource
138:         public void treeMasterDetailCompositeDoesNotLeak() {
139:                 final int expectedShellCount = Display.getDefault().getShells().length;
140:
141:                 final GenericEditor editor = open("test.ecore");
142:                 close(editor);
143:
144:                 final int actualShellCount = Display.getDefault().getShells().length;
145:                 assertThat("Limbo shell remains", actualShellCount, is(expectedShellCount));
146:         }
147:
148:         @Test
149:         @TestResource(value = { "template.ecore", "template.ecore" }, //
150:                 generator = { EcoreGen.class, EcoreGen.class })
151:         public void addElement() {
152:                 test(filePath -> {
153:                         final GenericEditor editor = open(filePath);
154:
155:                         final EClassifier newEClassifier = addEClassifier(editor);
156:                         reveal(editor, newEClassifier);
157:
158:                         close(editor);
159:                 });
160:         }
161:
162:         @Test
163:         @TestResource(value = { "template.ecore", "template.ecore" }, //
164:                 generator = { EcoreGen.class, EcoreGen.class })
165:         public void validation() {
166:                 Validation.active = true;
167:
168:                 test(filePath -> {
169:                         final GenericEditor editor = open(filePath);
170:
171:                         final EClassifier lastEClassifier = getLastEClassifier(editor);
172:                         reveal(editor, lastEClassifier);
173:
174:                         close(editor);
175:                 });
176:         }
177:
178:         //
179:         // Test framework
180:         //
181:
182:         /**
183:          * Import the test models indicated by the annotations on the {@code test} into the {@code project}.
184:          */
185:         void importTestModels(Description test, IProject project, IProgressMonitor monitor)
186:                 throws CoreException, IOException {
187:
188:                 final String[] paths = getTestResources(test);
189:                 final TestResource.Generator[] generators = getGenerators(test);
190:
191:                 final SubMonitor sub = SubMonitor.convert(monitor, paths.length);
192:                 final ResourceSet rset = new ResourceSetImpl();
193:
194:                 for (int i = 0; i < paths.length; i++) {
195:                         final String path = paths[i];
196:                         final TestResource.Generator generator = generators.length > i ? generators[i] : null;
197:
198:                         final URI uri = URI.createURI(
199:                                 "platform:/fragment/org.eclipse.emfforms.editor.ecore.test/data/" + path);
200:
201:                         final Resource resource = rset.getResource(uri, true);
202:                         resource
203:                                 .setURI(URI.createPlatformResourceURI(String.format("%s/%s", project.getName(), path), true));
204:                         if (generator != null) {
205:                                 generator.generate(resource, i, sub.newChild(1));
206:                         }
207:                         resource.save(null);
208:
209:                         if (generator == null) {
210:                                 // Didn't have a generator to advance the progress
211:                                 sub.worked(1);
212:                         }
213:                 }
214:
215:                 for (final Resource next : rset.getResources()) {
216:                         next.unload();
217:                 }
218:                 rset.getResources().clear();
219:                 rset.eAdapters().clear();
220:         }
221:
222:         /**
223:          * Get the test resources indicated by the annotation on the given {@code test}.
224:          *
225:          * @param test a test
226:          * @return its test resources
227:          */
228:         static String[] getTestResources(Description test) {
229:                 final TestResource testResource = test.getAnnotation(TestResource.class);
230:                 return testResource.value();
231:         }
232:
233:         /**
234:          * Get the test data generators indicated by the annotation on the given {@code test}.
235:          *
236:          * @param test a test
237:          * @return its test generators, or an empty array if none
238:          */
239:         static TestResource.Generator[] getGenerators(Description test) {
240:                 final TestResource testResource = test.getAnnotation(TestResource.class);
241:                 final Class<? extends TestResource.Generator>[] generatorClasses = testResource.generator();
242:                 final TestResource.Generator[] result = new TestResource.Generator[generatorClasses.length];
243:
244:                 // BEGIN COMPLEX CODE
245:                 for (int i = 0; i < generatorClasses.length; i++) {
246:                         try {
247:                                 result[i] = generatorClasses[i].newInstance();
248:                         } catch (final Exception e) {
249:                                 e.printStackTrace();
250:                                 fail("Failed to create test resource generator: " + e.getMessage());
251:                         }
252:                 }
253:                 // END COMPLEX CODE
254:
255:                 return result;
256:         }
257:
258:         @BeforeClass
259:         public static void closeIntroView() {
260:                 final IViewPart introView = getActivePage().findView("org.eclipse.ui.internal.introview");
261:                 if (introView != null) {
262:                         introView.getSite().getPage().hideView(introView);
263:                 }
264:         }
265:
266:         @After
267:         public void closeAllEditors() {
268:                 getActivePage().closeAllEditors(false);
269:         }
270:
271:         @After
272:         public void ensureNoValidation() {
273:                 Validation.active = false;
274:         }
275:
276:         /**
277:          * Run an {@code experiment} on both the small- and the large-scale test model, measuring
278:          * the performance of each, and compare the performance to verify that it's not worse
279:          * than the expected worst case multiplier.
280:          *
281:          * @param experiment the experiment to run at each scale of model
282:          */
283:         final void test(Consumer<String> experiment) {
284:                 PerformanceClock.test(ITERATIONS, WORST_CASE_MULTIPLIER, () -> SMALL_FILE_NAME, () -> LARGE_FILE_NAME,
285:                         experiment);
286:         }
287:
288:         GenericEditor open(String fileName) {
289:                 return open(project.getProject().getFile(fileName));
290:         }
291:
292:         GenericEditor open(IFile file) {
293:                 try {
294:                         final IEditorPart result = IDE.openEditor(getActivePage(), file,
295:                                 "org.eclipse.emfforms.editor.ecore.test.Editor");
296:                         return (GenericEditor) result;
297:                 } catch (final PartInitException e) {
298:                         e.printStackTrace();
299:                         fail("Failed to open editor: " + e.getMessage());
300:                         return null; // Unreachable
301:                 } finally {
302:                         flushUIEvents();
303:                 }
304:         }
305:
306:         void close(IEditorPart editor) {
307:                 editor.getSite().getPage().closeEditor(editor, false);
308:
309:                 flushUIEvents();
310:         }
311:
312:         static IWorkbenchPage getActivePage() {
313:                 final IWorkbench bench = PlatformUI.getWorkbench();
314:                 IWorkbenchWindow window = bench.getActiveWorkbenchWindow();
315:                 if (window == null) {
316:                         window = bench.getWorkbenchWindows()[0];
317:                 }
318:                 return window.getActivePage();
319:         }
320:
321:         /**
322:          * Add an {@link EClassifier} in an {@code editor}.
323:          *
324:          * @return the newly added classifier
325:          */
326:         EClassifier addEClassifier(GenericEditor editor) {
327:                 final EditingDomain domain = editor.getEditingDomain();
328:                 final EPackage ePackage = (EPackage) EcoreUtil.getObjectByType(
329:                         domain.getResourceSet().getResources().get(0).getContents(),
330:                         EcorePackage.Literals.EPACKAGE);
331:                 final EClassifier result = EcoreFactory.eINSTANCE.createEClass();
332:                 result.setName("NewClass");
333:                 final Command command = AddCommand.create(domain, ePackage, EcorePackage.Literals.EPACKAGE__ECLASSIFIERS,
334:                         result);
335:                 domain.getCommandStack().execute(command);
336:
337:                 flushUIEvents();
338:
339:                 return result;
340:         }
341:
342:         /**
343:          * Get the last {@link EClassifier} in an {@code editor}.
344:          *
345:          * @return the last classifier
346:          */
347:         EClassifier getLastEClassifier(GenericEditor editor) {
348:                 final EditingDomain domain = editor.getEditingDomain();
349:                 final EPackage ePackage = (EPackage) EcoreUtil.getObjectByType(
350:                         domain.getResourceSet().getResources().get(0).getContents(),
351:                         EcorePackage.Literals.EPACKAGE);
352:
353:                 final List<EClassifier> classifiers = ePackage.getEClassifiers();
354:                 return classifiers.get(classifiers.size() - 1);
355:         }
356:
357:         /**
358:          * Reveal an {@code object} in the {@code editor}.
359:          */
360:         void reveal(GenericEditor editor, EObject object) {
361:                 editor.reveal(object);
362:                 flushUIEvents();
363:         }
364:
365:         /**
366:          * Generate EClassifiers in an Ecore model.
367:          */
368:         static void generateEClassifiers(Resource resource, String name, int size, boolean forValidation,
369:                 IProgressMonitor monitor)
370:                 throws CoreException, IOException {
371:
372:                 final SubMonitor sub = SubMonitor.convert(monitor, size);
373:
374:                 final EPackage ePackage = (EPackage) EcoreUtil.getObjectByType(resource.getContents(),
375:                         EcorePackage.Literals.EPACKAGE);
376:                 resource.setURI(resource.getURI().trimSegments(1).appendSegment(name));
377:                 generateClassifiers(ePackage, size, forValidation, sub.newChild(size));
378:         }
379:
380:         static void generateClassifiers(EPackage ePackage, int count, boolean forValidation, IProgressMonitor monitor) {
381:                 monitor.beginTask("Generating station content...", count);
382:
383:                 final List<EClassifier> classifiers = ePackage.getEClassifiers();
384:                 for (int i = 0; i < count; i++) {
385:                         EClassifier classifier;
386:                         if (random.nextDouble() >= 0.8) {
387:                                 // Relatively smaller proportion of data types
388:                                 classifier = random.nextBoolean() ? EcoreFactory.eINSTANCE.createEEnum()
389:                                         : EcoreFactory.eINSTANCE.createEDataType();
390:
391:                                 if (classifier instanceof EEnum) {
392:                                         // It must have at least one literal
393:                                         final EEnumLiteral literal = EcoreFactory.eINSTANCE.createEEnumLiteral();
394:                                         literal.setName("literal");
395:                                         ((EEnum) classifier).getELiterals().add(literal);
396:                                 } else {
397:                                         // It must have an instance class
398:                                         classifier.setInstanceClass(Void.class);
399:                                 }
400:                         } else {
401:                                 final EClass eClass = EcoreFactory.eINSTANCE.createEClass();
402:                                 eClass.setAbstract(random.nextBoolean());
403:                                 classifier = eClass;
404:                         }
405:
406:                         // Only allow digits in tests with validation, so that otherwise we won't
407:                         // waste effort on presenting validation errors for EClassifiers whose names
408:                         // start with numeric digits (which makes for invalid Java names)
409:                         classifier.setName(randomName(forValidation));
410:                         classifiers.add(classifier);
411:                 }
412:         }
413:
414:         static String randomName(boolean withDigits) {
415:                 // We have a constraint that checks for even number of characters in strings in abstract classes
416:                 final int length = random.nextBoolean() ? 9 : 10;
417:                 final char[] result = new char[length];
418:
419:                 final char[] charExtent = withDigits ? NAME_CHARS : NAME_CHARS_NO_DIGITS;
420:
421:                 for (int i = 0; i < length; i++) {
422:                         result[i] = charExtent[random.nextInt(charExtent.length)];
423:                 }
424:
425:                 return new String(result);
426:         }
427:
428:         static void flushUIEvents() {
429:                 final Display display = Display.getCurrent();
430:                 while (display.readAndDispatch()) {
431:                         // Nothing to do
432:                 }
433:         }
434:
435:         //
436:         // Nested types
437:         //
438:
439:         /**
440:          * Annotates a test with the resources that it needs to import into the test project.
441:          */
442:         @Target(ElementType.METHOD)
443:         @Retention(RetentionPolicy.RUNTIME)
444:         @interface TestResource {
445:                 /**
446:                  * Paths within the {@code data/} folder of resources to import into the test project.
447:                  */
448:                 String[] value() default { "test.ecore" };
449:
450:                 /**
451:                  * Optional generators to run on each test resource. If specified, there must be
452:                  * exactly one value per resource in the {@link #value()}.
453:                  *
454:                  * @return resource generators
455:                  */
456:                 Class<? extends Generator>[] generator() default {};
457:
458:                 interface Generator {
459:                         void generate(Resource resource, int index, IProgressMonitor monitor) throws IOException, CoreException;
460:                 }
461:         }
462:
463:         public final class ProjectRule extends TestWatcher {
464:                 private IProject project;
465:
466:                 private List<IFile> files;
467:
468:                 /**
469:                  * @return the project
470:                  */
471:                 public IProject getProject() {
472:                         return project;
473:                 }
474:
475:                 /**
476:                  * @return the files
477:                  */
478:                 public List<IFile> getFiles() {
479:                         return files;
480:                 }
481:
482:                 @Override
483:                 protected void starting(final Description description) {
484:                         project = ResourcesPlugin.getWorkspace().getRoot().getProject(description.getMethodName());
485:
486:                         try {
487:                                 if (project.exists()) {
488:                                         flushUIEvents();
489:                                         project.delete(true, null);
490:                                 }
491:                                 flushUIEvents();
492:                                 project.create(null);
493:                                 project.open(null);
494:
495:                                 project.getWorkspace().run(new IWorkspaceRunnable() {
496:
497:                                         @Override
498:                                         public void run(IProgressMonitor monitor) throws CoreException {
499:                                                 try {
500:                                                         importTestModels(description, project, monitor);
501:                                                 } catch (final IOException e) {
502:                                                         final Bundle bundle = FrameworkUtil.getBundle(getClass());
503:                                                         String message = e.getMessage();
504:                                                         if (message == null || message.isEmpty()) {
505:                                                                 message = "Unknown I/O exception.";
506:                                                         }
507:                                                         throw new CoreException(
508:                                                                 new Status(IStatus.ERROR, bundle.getSymbolicName(), message, e));
509:                                                 }
510:                                         }
511:                                 }, new NullProgressMonitor());
512:
513:                                 flushUIEvents();
514:
515:                                 // Generators can change the file names
516:                                 files = new ArrayList<IFile>(getTestResources(description).length);
517:                                 for (final IResource next : project.members()) {
518:                                         if (next.getType() == IResource.FILE) {
519:                                                 files.add((IFile) next);
520:                                         }
521:                                 }
522:                         } catch (final CoreException e) {
523:                                 e.printStackTrace();
524:                                 fail("Failed to create test project: " + e.getStatus().getMessage());
525:                         }
526:                 }
527:
528:                 @Override
529:                 protected void finished(Description description) {
530:                         flushUIEvents();
531:                         try {
532:                                 project.delete(true, null);
533:                         } catch (final CoreException e) {
534:                                 e.printStackTrace();
535:                         }
536:                         flushUIEvents();
537:                 }
538:         }
539:
540:         /**
541:          * Subclass of the <em>Ecore Editor</em> that injects view-model properties to select our
542:          * custom {@link EPackage} view.
543:          */
544:         public static final class Editor extends EcoreEditor {
545:                 @Override
546:                 protected TreeMasterDetailComposite createTreeMasterDetail(Composite composite, Object editorInput,
547:                         CreateElementCallback createElementCallback) {
548:
549:                         final TreeMasterDetailComposite result = super.createTreeMasterDetail(composite, editorInput,
550:                                 createElementCallback);
551:                         result.addViewModelPropertiesUpdateCallback(new ViewModelPropertiesUpdateCallback() {
552:
553:                                 @Override
554:                                 public void updateViewModelProperties(VViewModelProperties properties) {
555:                                         // Pick our special views
556:                                         properties.addNonInheritableProperty("perftest", true);
557:                                 }
558:                         });
559:                         return result;
560:                 }
561:
562:         }
563:
564:         /**
565:          * A dummy validation provider that complains about an {@link EClass} that is
566:          * a {@link EClass#isAbstract() abstract} and has a {@link ENamedElement#getName() name}
567:          * that has an odd number of characters.
568:          */
569:         public static final class Validation implements ValidationProvider {
570:                 private static boolean active;
571:
572:                 @Override
573:                 public List<Diagnostic> validate(EObject eObject) {
574:                         if (!active || !(eObject instanceof EClass)) {
575:                                 return Collections.emptyList();
576:                         }
577:
578:                         Diagnostic result;
579:
580:                         final EClass eClass = (EClass) eObject;
581:                         if (!eClass.isAbstract() || !hasOddLength(eClass.getName())) {
582:                                 result = Diagnostic.OK_INSTANCE;
583:                         } else {
584:                                 result = new BasicDiagnostic(Diagnostic.ERROR, "test", 1,
585:                                         "Name has odd length.", new Object[] { eClass, EcorePackage.Literals.ENAMED_ELEMENT__NAME });
586:                         }
587:
588:                         return Collections.singletonList(result);
589:                 }
590:
591:                 private boolean hasOddLength(String string) {
592:                         return string != null && string.length() % 2 != 0;
593:                 }
594:         }
595:
596:         /**
597:          * Generates from the template resource two Ecore models: a small-scale model and a large-scale model.
598:          */
599:         static final class EcoreGen implements TestResource.Generator {
600:
601:                 @Override
602:                 public void generate(Resource resource, int index, IProgressMonitor monitor) throws IOException, CoreException {
603:                         // Is it a validation test?
604:                         final boolean forValidation = resource.getURI().segmentsList().contains("validation");
605:                         if (index == 0) {
606:                                 generateEClassifiers(resource, SMALL_FILE_NAME, SMALL_SCALE, forValidation, monitor);
607:                         } else {
608:                                 generateEClassifiers(resource, LARGE_FILE_NAME, LARGE_SCALE, forValidation, monitor);
609:                         }
610:                 }
611:
612:         }
613: }