/*
* Copyright 2003-2016 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.classloading;
import jetbrains.mps.module.ModuleClassLoaderIsNullException;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.Solution;
import jetbrains.mps.project.facets.JavaModuleFacet;
import jetbrains.mps.project.facets.JavaModuleFacetImpl;
import jetbrains.mps.project.structure.modules.Dependency;
import jetbrains.mps.project.structure.modules.SolutionDescriptor;
import jetbrains.mps.project.structure.modules.SolutionKind;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.testbench.ModuleMpsTest;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.Reference;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.FacetsFacade;
import org.jetbrains.mps.openapi.module.FacetsFacade.FacetFactory;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleFacet;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class ModulesReloadTest extends ModuleMpsTest {
private static FacetFactory ourOldFacetFactory;
private final ClassLoaderManager myManager = ClassLoaderManager.getInstance();
private static final String CLASS_TO_LOAD = "Test";
private static final File TEMP_DIR = createTempDir();
private static final String TEMP_DIR_PATH = getTempDirPath();
private static final FacetFactory FACET_FACTORY = TestJavaModuleFacet::new;
private static File createTempDir() {
File tempDir = FileUtil.createTmpDir();
Assume.assumeTrue("Cannot write the " + tempDir + " directory", tempDir.canRead());
return tempDir;
}
private static String getTempDirPath() {
try {
return TEMP_DIR.getCanonicalPath();
} catch (IOException e) {
throw new IOError(e);
}
}
@BeforeClass
public static void setUp() {
new TestClassFileCreator(CLASS_TO_LOAD, TEMP_DIR_PATH).create();
attachTestJavaFacetFactory();
}
@AfterClass
public static void tearDown() {
FileUtil.delete(TEMP_DIR);
detachTestJavaFacetFactory();
}
private static void attachTestJavaFacetFactory() {
ourOldFacetFactory = FacetsFacade.getInstance().getFacetFactory(JavaModuleFacet.FACET_TYPE);
FacetsFacade.getInstance().removeFactory(ourOldFacetFactory);
FacetsFacade.getInstance().addFactory(JavaModuleFacet.FACET_TYPE, FACET_FACTORY);
}
private static void detachTestJavaFacetFactory() {
FacetsFacade.getInstance().removeFactory(FACET_FACTORY);
FacetsFacade.getInstance().addFactory(JavaModuleFacet.FACET_TYPE, ourOldFacetFactory);
}
@Test
public void testLanguageIsLoadable() {
final Language language = createLanguage();
Assert.assertTrue(safeGetClass(language, CLASS_TO_LOAD) == null);
addClassTo(language);
getModelAccess().runWriteAction(() -> {
language.reload();
Assert.assertTrue(classIsLoadableFromModule(language));
});
}
@Test
public void testGeneratorIsLoadable() {
final Generator generator = createGenerator();
Assert.assertTrue(safeGetClass(generator, CLASS_TO_LOAD) == null);
addClassTo(generator);
getModelAccess().runWriteAction(() -> {
generator.reload();
Assert.assertTrue(classIsLoadableFromModule(generator));
});
}
@Test
public void testPluginSolutionIsLoadable() {
final Solution solution = createSolution(SolutionKind.PLUGIN_CORE);
addClassTo(solution);
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
solution.reload();
}
});
Assert.assertTrue(classIsLoadableFromModule(solution));
}
@Test
public void testNotLoadableDepsAreNotLoadable() {
final Solution solution = createSolution(SolutionKind.NONE);
addClassTo(solution);
final Language l1 = createLanguage();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
myManager.reloadModule(solution);
l1.addDependency(solution.getModuleReference(), false);
myManager.reloadModule(solution);
// Assert.assertFalse(classIsLoadableFromModule(l1)); // the class must be available already here FIXME: enable after 3.2
}
});
}
@Test
public void testNonPluginSolutionIsNotLoadable() {
final Solution solution = createSolution(SolutionKind.NONE);
addClassTo(solution);
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
myManager.reloadModule(solution);
// Assert.assertFalse(classIsLoadableFromModule(solution)); FIXME: enable after 3.2
}
});
// Assert.assertFalse(classIsLoadableFromModule(solution));
}
@Test
public void testReloadNonLoadableSolution() {
final Solution solution = createSolution(SolutionKind.NONE);
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
solution.reload();
myManager.reloadModule(solution);
}
});
}
@Test
public void testReloadingSolutionKinds() {
final Solution solution = createSolution(SolutionKind.NONE);
addClassTo(solution);
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
SolutionDescriptor moduleDescriptor = solution.getModuleDescriptor();
assert moduleDescriptor != null;
moduleDescriptor.setKind(SolutionKind.NONE);
myManager.reloadModule(solution);
Assert.assertTrue(myManager.getModulesWatcher().isModuleWatched(solution));
// Assert.assertFalse(classIsLoadableFromModule(solution)); FIXME: enable after 3.2
moduleDescriptor.setKind(SolutionKind.PLUGIN_CORE);
myManager.reloadModule(solution);
Assert.assertTrue(classIsLoadableFromModule(solution));
Assert.assertTrue(myManager.getModulesWatcher().isModuleWatched(solution));
moduleDescriptor.setKind(SolutionKind.NONE);
myManager.reloadModule(solution);
Assert.assertTrue(myManager.getModulesWatcher().isModuleWatched(solution));
// Assert.assertFalse(classIsLoadableFromModule(solution)); FIXME: enable after 3.2
}
});
}
@Test
public void testDepsAreLoadable1() {
final Language l1 = createLanguage();
final Language l2 = createLanguage();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
addClassTo(l2);
l2.reload();
l1.addDependency(l2.getModuleReference(), false);
Assert.assertTrue(classIsLoadableFromModule(l1)); // the class must be available already here
}
});
}
@Test
public void testDepsAreLoadable2() {
final Language l = createLanguage();
final Solution s = createSolution();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
addClassTo(s);
l.addDependency(s.getModuleReference(), false);
// Assert.assertFalse(classIsLoadableFromModule(l)); FIXME turn on after 3.2
SolutionDescriptor moduleDescriptor = s.getModuleDescriptor();
assert moduleDescriptor != null;
moduleDescriptor.setKind(SolutionKind.PLUGIN_CORE);
s.reload();
Assert.assertTrue(classIsLoadableFromModule(l)); // the class must be available already here
}
});
}
@Test
public void testNonLoadableDepsThrows() {
final Language l = createLanguage();
final Solution s = createSolution(SolutionKind.NONE);
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
addClassTo(s);
l.addDependency(s.getModuleReference(), false);
// Assert.assertFalse(classIsLoadableFromModule(l)); // the class must be available already here FIXME: enable after 3.2
}
});
}
@Test
public void testBackDepsReload() {
final Language l1 = createLanguage();
final Language l2 = createLanguage();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l1.addDependency(l2.getModuleReference(), false);
l1.reload();
}
});
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
addClassTo(l2);
l2.reload();
Assert.assertTrue(classIsLoadableFromModule(l1));
Assert.assertTrue(classIsLoadableFromModule(l2));
}
});
}
@Test
public void testLanguageAddRemove() {
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
final Language language = createLanguage();
removeModule(language);
}
});
}
@Test
public void testUnload1() {
final Language l1 = createLanguage();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
removeModule(l1);
Assert.assertTrue(l1.getClassLoader() == null);
Assert.assertTrue(!myManager.getModulesWatcher().isModuleWatched(l1));
}
});
Assert.assertTrue(l1.getClassLoader() == null);
}
@Test
public void testModuleRemoval() {
final Language l1 = createLanguage();
final Language l2 = createLanguage();
final Language l3 = createLanguage();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l1.addDependency(l2.getModuleReference(), false);
l2.addDependency(l3.getModuleReference(), false);
addClassTo(l2);
l2.reload();
Assert.assertTrue(classIsLoadableFromModule(l1));
removeModule(l3);
Assert.assertFalse(classIsLoadableFromModule(l1));
Assert.assertFalse(myManager.getModulesWatcher().isModuleWatched(l3));
}
});
Assert.assertFalse(classIsLoadableFromModule(l1));
}
@Test
public void testModuleRecreation1() {
final Language l1 = createLanguage();
final Language[] l2 = new Language[1];
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
addClassTo(l1);
l1.reload();
Assert.assertTrue(classIsLoadableFromModule(l1));
removeModule(l1);
l2[0] = createLanguage(l1.getModuleDescriptor().getId(), l1.getModuleName()); // the same
Assert.assertTrue(l2[0].getClassLoader() != null);
}
});
Assert.assertTrue(l2[0].getClassLoader() != null);
}
@Test
public void testModuleRecreation2() {
final Language[] l = new Language[1];
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l[0] = createLanguage();
addClassTo(l[0]);
l[0].reload();
Assert.assertTrue(classIsLoadableFromModule(l[0]));
removeModule(l[0]);
}
});
Assert.assertTrue(l[0].getClassLoader() == null);
}
@Test
public void testModuleRecreation3() {
final Language[] l = new Language[3];
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l[0] = createLanguage();
l[1] = createLanguage();
l[0].addDependency(l[1].getModuleReference(), false);
removeModule(l[1]);
l[2] = createLanguage(l[1].getModuleDescriptor().getId(), l[1].getModuleName()); // the same
addClassTo(l[2]);
l[2].reload();
Assert.assertTrue(classIsLoadableFromModule(l[0]));
}
});
Assert.assertTrue(classIsLoadableFromModule(l[0]));
}
@Test
public void testLanguageRuntimeIsLoadable() {
final Reference<Language> language = new Reference<Language>();
final Reference<Solution> runtime = new Reference<Solution>();
final Reference<Solution> solution = new Reference<Solution>();
getModelAccess().runWriteAction(new Runnable() {
@SuppressWarnings("ConstantConditions")
@Override
public void run() {
runtime.set(createSolution(SolutionKind.PLUGIN_OTHER));
addClassTo(runtime.get());
Assert.assertTrue(classIsLoadableFromModule(runtime.get()));
}
});
// contents of the run() method used to execute without write action, Alex, please check if it's correct to have it in model command.
// I need it because add language to attached module requires model access, besides, creation of solutions/languages in the test
// happens inside a command, thus I don't see reason to do it otherwise here.
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
language.set(createLanguage(runtime.get().getModuleReference()));
solution.set(createSolution(SolutionKind.PLUGIN_OTHER));
addUsedLanguage(solution.get(), language.get());
}
});
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
solution.get().reload();
Assert.assertTrue(classIsLoadableFromModule(solution.get()));
}
});
}
@Test
public void testLanguageRuntimeIsReloadable() {
final Reference<Language> language = new Reference<Language>();
final Reference<Solution> runtime = new Reference<Solution>();
final Reference<Solution> solution = new Reference<Solution>();
getModelAccess().runWriteAction(new Runnable() {
@SuppressWarnings("ConstantConditions")
@Override
public void run() {
runtime.set(createSolution(SolutionKind.PLUGIN_OTHER));
addClassTo(runtime.get());
Assert.assertTrue(classIsLoadableFromModule(runtime.get()));
language.set(createLanguage(runtime.get().getModuleReference()));
solution.set(createSolution(SolutionKind.PLUGIN_OTHER));
addUsedLanguage(solution.get(), language.get());
removeModule(language.get());
Language sameLanguage = createLanguage(language.get().getModuleDescriptor().getId(), language.get().getModuleName(), runtime.get().getModuleReference()); // the same
solution.get().reload();
Assert.assertTrue(classIsLoadableFromModule(solution.get()));
}
});
}
@Test
public void testModuleDeps() {
final Language[] l = new Language[2];
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l[0] = createLanguage();
l[1] = createLanguage();
l[0].addDependency(l[1].getModuleReference(), false);
addClassTo(l[1]);
l[1].reload();
Assert.assertTrue(classIsLoadableFromModule(l[0]));
}
});
Assert.assertTrue(classIsLoadableFromModule(l[0]));
}
@Test
public void testDisposedDepsIsNotValidForCL() {
final Language l1 = createLanguage();
addClassTo(l1);
final Language l2 = createLanguage();
final Language l3 = createLanguage();
final Reference<Dependency> dep12 = new Reference<Dependency>();
final Reference<Dependency> dep13 = new Reference<Dependency>();
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
dep12.set(l1.addDependency(l2.getModuleReference(), false));
dep13.set(l1.addDependency(l3.getModuleReference(), false));
}
});
Assert.assertTrue(classIsLoadableFromModule(l1));
removeModule(l2);
Assert.assertTrue(!classIsLoadableFromModule(l1));
removeModule(l3);
Assert.assertTrue(!classIsLoadableFromModule(l1));
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l1.removeDependency(dep12.get());
}
});
Assert.assertTrue(!classIsLoadableFromModule(l1)); // still no, obviously
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
l1.removeDependency(dep13.get());
}
});
Assert.assertTrue(classIsLoadableFromModule(l1));
}
/**
* adds {@link #CLASS_TO_LOAD} to class path of the given module
*/
private void addClassTo(SModule module) {
TestJavaModuleFacet facet = module.getFacet(TestJavaModuleFacet.class);
assert facet != null;
facet.setLibClassPath(TEMP_DIR_PATH);
}
private boolean classIsLoadableFromModule(ReloadableModule module) {
return safeGetClass(module, CLASS_TO_LOAD) != null;
}
@Nullable
private static Class<?> safeGetClass(ReloadableModule module, String classFqName) {
try {
return module.getClass(classFqName);
} catch (ClassNotFoundException ignored) {
return null;
} catch (ModuleClassLoaderIsNullException ignored) {
return null;
}
}
/**
* My personal JavaModuleFacet implementation, which allows to reset library class path and compileInMps flag.
*/
private static class TestJavaModuleFacet extends JavaModuleFacetImpl implements JavaModuleFacet {
private String myLibClassPath = null;
private boolean myCompileInMps = true;
public TestJavaModuleFacet() {
}
@Override
public boolean isCompileInMps() {
return myCompileInMps;
}
@Override
@Nullable
public IFile getClassesGen() {
return null;
}
@Override
public Set<String> getLibraryClassPath() {
Set<String> result = new HashSet<String>();
if (myLibClassPath != null) result.add(myLibClassPath);
return result;
}
public void setLibClassPath(@Nullable String newPath) {
myLibClassPath = newPath;
}
public void setCompileInMps(boolean value) {
myCompileInMps = value;
}
}
private Solution createSolution(SolutionKind kind) {
Solution solution = super.createSolution();
SolutionDescriptor moduleDescriptor = solution.getModuleDescriptor();
moduleDescriptor.setKind(kind);
return solution;
}
}