/* * Copyright 2003-2015 JetBrains s.r.o. * * 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 jetbrains.mps.repo; import jetbrains.mps.CoreMpsTest; import jetbrains.mps.extapi.model.SModelBase; import jetbrains.mps.extapi.module.SRepositoryExt; import jetbrains.mps.extapi.module.SRepositoryRegistry; import jetbrains.mps.project.ModuleId; import jetbrains.mps.project.Project; import jetbrains.mps.project.Solution; import jetbrains.mps.project.StubSolution; import jetbrains.mps.project.structure.ProjectStructureModule; import jetbrains.mps.project.structure.modules.SolutionDescriptor; import jetbrains.mps.smodel.BaseMPSModuleOwner; import jetbrains.mps.smodel.MPSModuleOwner; import jetbrains.mps.smodel.MPSModuleRepository; import jetbrains.mps.smodel.SModel; import jetbrains.mps.smodel.SModelId; import jetbrains.mps.smodel.TrivialModelDescriptor; import jetbrains.mps.tool.environment.EnvironmentConfig; import jetbrains.mps.tool.environment.MpsEnvironment; import jetbrains.mps.util.Computable; import jetbrains.mps.util.ComputeRunnable; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.module.SRepositoryAttachListener; import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter; import org.jetbrains.mps.openapi.module.SRepositoryListener; import org.jetbrains.mps.openapi.module.SRepositoryListenerBase; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import org.junit.After; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; /** * Check contract of SRepositoryListener and SRepositoryContentAdapter. * Lives in [testbench], not [smodel], as it depends from CoreMpsTest (though perhaps might have add [testbench] as test-only dependency for [smodel]???) * @author Artem Tikhomirov */ public class RepoListenerTest extends CoreMpsTest { private Project myProject; @BeforeClass public static void prepare() { MpsEnvironment.getOrCreate(EnvironmentConfig.defaultConfig()); } @After public void tearDown() { closeProject(); } private Project createProject() { closeProject(); return myProject = getEnvironment().createEmptyProject(); } private void closeProject() { if (myProject != null) { myProject.dispose(); myProject = null; } } // FIXME see RepoListenerRegistrar and considerations about true necessity of read lock private static void attach(final SRepository repo, final SRepositoryListener listener) { repo.getModelAccess().runReadAction(new Runnable() { @Override public void run() { repo.addRepositoryListener(listener); } }); } private static void detach(final SRepository repo, final SRepositoryListener listener) { repo.getModelAccess().runReadAction(new Runnable() { @Override public void run() { repo.removeRepositoryListener(listener); } }); } /** * Test SRepositoryAttachListener is notified when added to a repo */ @Test public void testAttach() { final Project project = createProject(); final AttachRepoListener l = new AttachRepoListener(); attach(project.getRepository(), l); l.checkStarted(1); l.checkStopped(0); detach(project.getRepository(), l); l.checkStarted(1); l.checkStopped(1); closeProject(); } /** * Test SRepositoryAttachListener, added globally, is notified there's a new repo. * NOTE, this test shall fail once we drop global MPSModuleRepository instance */ @Test public void testGlobalAttach() { final AttachRepoListener l1 = new AttachRepoListener(); SRepositoryRegistry.getInstance().addGlobalListener(l1); l1.checkStarted(1); // MPSModuleRepository.INSTANCE l1.checkStopped(0); createProject(); // project repo mimics global repo now, listener is attached only once, hence we observe events of 1 repository instead of 2. final int distinctRepositories = 1; // FIXME =2 once ProjectRepository is distinct from global (or there's no global?) // l1.checkStarted(2); // global + project repo l1.checkStarted(distinctRepositories); l1.checkStopped(0); // final AttachRepoListener l2 = new AttachRepoListener(); SRepositoryRegistry.getInstance().addGlobalListener(l2); l1.checkStarted(distinctRepositories); l1.checkStopped(0); l2.checkStarted(distinctRepositories); // == that of l1 starts to the date, == amount of our available repositories l2.checkStopped(0); SRepositoryRegistry.getInstance().removeGlobalListener(l2); l1.checkStarted(distinctRepositories); l1.checkStopped(0); // l1 is not notified on l2 removal l2.checkStarted(distinctRepositories); l2.checkStopped(distinctRepositories); // l2 is removed from both available repositories, global+project // closeProject(); l1.checkStarted(distinctRepositories); l1.checkStopped(distinctRepositories-1); // project repo is gone, 1 notification l2.checkStarted(distinctRepositories); // l2 is detached, shall not get any further notifications l2.checkStopped(distinctRepositories); // --"-- SRepositoryRegistry.getInstance().removeGlobalListener(l1); l1.checkStarted(distinctRepositories); l1.checkStopped(distinctRepositories); // notified for global repo l2.checkStarted(distinctRepositories); // l2 is detached, shall not get any further notifications l2.checkStopped(distinctRepositories); // --"-- } /** * Test SRepositoryListener is notified when a module is added to/removed from a repo */ @Test public void testModuleAddRemove() { final Project project = createProject(); final AttachRepoListener l = new AttachRepoListener(); final SRepositoryExt repository = (SRepositoryExt) project.getRepository(); attach(project.getRepository(), l); l.checkModuleEvents(0, 0, 0); final BaseMPSModuleOwner moduleOwner = new BaseMPSModuleOwner(); // final Solution solution = new CreateSolution(repository, moduleOwner).execute(); l.checkModuleEvents(1, 0, 0); new RemoveModule(repository, solution, moduleOwner).execute(); l.checkModuleEvents(1, 1, 1); // detach(project.getRepository(), l); l.checkModuleEvents(1, 1, 1); closeProject(); } /** * Test SRepositoryContentAdapter is notified when a model is added to/removed from a module * It's vital to get SModuleListener notified when a model is removed even as part of module un-registration sequence * as otherwise we might end up with state caches */ @Test public void testContentAdapterModelAddRemove() { final Project project = createProject(); final ContentAdapter l = new ContentAdapter(); final SRepositoryExt repository = (SRepositoryExt) project.getRepository(); attach(project.getRepository(), l); final BaseMPSModuleOwner moduleOwner = new BaseMPSModuleOwner(); // final Solution solution = new CreateSolution(repository, moduleOwner).execute(); // FIXME attach solution with existing model (1+), check content adapter got a chance to attach to a model and modelAdded is dispatched // // add model, check content adapter is notified, modelAdded is fired project.getModelAccess().runWriteAction(new Runnable() { @Override public void run() { solution.registerModel(createModel(solution.getModuleReference(), "m1")); solution.registerModel(createModel(solution.getModuleReference(), "m2")); } }); l.checkModelEvents(2, 0, 0); // // remove model, -"-, modelRemoved is fired project.getModelAccess().runWriteAction(new Runnable() { @Override public void run() { solution.unregisterModel((SModelBase) solution.getModels().get(0)); } }); l.checkModelEvents(2, 1, 1); // // remove module, check modelRemoved is fired new RemoveModule(repository, solution, moduleOwner).execute(); // l.checkModelEvents(2, 2, 2); // FIXME at the moment, removal of a module doesn't trigger events for modelRemoved l.checkModelEvents(2, 1, 1); closeProject(); } private SModelBase createModel(SModuleReference moduleRef, String name) { // FIXME perhaps, can re-use TestModelFactory and its TestModelBase? return new TrivialModelDescriptor(new SModel(PersistenceFacade.getInstance().createModelReference(moduleRef, SModelId.generate(), name))); } private static class CreateSolution implements Computable<Solution> { private final SRepositoryExt myRepository; private final MPSModuleOwner myModuleOwner; public CreateSolution(SRepositoryExt repository, MPSModuleOwner moduleOwner) { myRepository = repository; myModuleOwner = moduleOwner; } @Override public Solution compute() { SolutionDescriptor descriptor = new SolutionDescriptor(); descriptor.setNamespace("Solution"); descriptor.setId(ModuleId.regular()); return StubSolution.newInstance(myRepository, descriptor, myModuleOwner); } public Solution execute() { ComputeRunnable<Solution> cr = new ComputeRunnable<Solution>(this); myRepository.getModelAccess().runWriteAction(cr); return cr.getResult(); } } private static class RemoveModule implements Runnable { private final SRepositoryExt myRepository; private final SModule myModule; private final MPSModuleOwner myOwner; public RemoveModule(SRepositoryExt repository, SModule module, MPSModuleOwner owner) { myRepository = repository; myModule = module; myOwner = owner; } @Override public void run() { myRepository.unregisterModule(myModule, myOwner); } public void execute() { myRepository.getModelAccess().runWriteAction(this); } } private static class AttachRepoListener extends SRepositoryListenerBase implements SRepositoryAttachListener { private int myStartListen = 0, myStopListen = 0; private int myModuleAdded = 0, myModuleRemoved = 0, myModuleBeforeRemoved = 0; AttachRepoListener() { } @Override public void startListening(@NotNull SRepository repository) { myStartListen++; } @Override public void stopListening(@NotNull SRepository repository) { myStopListen++; } void checkStarted(int count) { Assert.assertEquals(count, myStartListen); } void checkStopped(int count) { Assert.assertEquals(count, myStopListen); } @Override public void moduleAdded(@NotNull SModule module) { myModuleAdded++; } @Override public void beforeModuleRemoved(@NotNull SModule module) { myModuleBeforeRemoved++; } @Override public void moduleRemoved(@NotNull SModuleReference module) { myModuleRemoved++; } void checkModuleEvents(int added, int beforeRemoved, int removed) { Assert.assertEquals(added, myModuleAdded); Assert.assertEquals(beforeRemoved, myModuleBeforeRemoved); Assert.assertEquals(removed, myModuleRemoved); } } private static class ContentAdapter extends SRepositoryContentAdapter { private int myModelAdded = 0, myModelRemoved = 0, myModelBeforeRemoved = 0; @Override protected boolean isIncluded(SModule module) { // when test module is added, ProjectStructureModule adds a model for it, and obscures results we intend to observe, // thus we don't track it altogether return !(module instanceof ProjectStructureModule); } @Override public void modelAdded(SModule module, org.jetbrains.mps.openapi.model.SModel model) { super.modelAdded(module, model); myModelAdded++; } @Override public void beforeModelRemoved(SModule module, org.jetbrains.mps.openapi.model.SModel model) { super.beforeModelRemoved(module, model); myModelBeforeRemoved++; } @Override public void modelRemoved(SModule module, SModelReference ref) { myModelRemoved++; } void checkModelEvents(int added, int beforeRemoved, int removed) { Assert.assertEquals(added, myModelAdded); Assert.assertEquals(beforeRemoved, myModelBeforeRemoved); Assert.assertEquals(removed, myModelRemoved); } } }