/*
* 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.generator.test;
import com.intellij.openapi.application.PathManager;
import jetbrains.mps.extapi.model.GeneratableSModel;
import jetbrains.mps.extapi.module.SRepositoryExt;
import jetbrains.mps.generator.GenerationCacheContainer.FileBasedGenerationCacheContainer;
import jetbrains.mps.generator.GenerationFacade;
import jetbrains.mps.generator.GenerationOptions;
import jetbrains.mps.generator.GenerationStatus;
import jetbrains.mps.generator.ModelDigestUtil;
import jetbrains.mps.generator.TransientModelsProvider;
import jetbrains.mps.generator.impl.DefaultIncrementalStrategy;
import jetbrains.mps.generator.impl.DefaultNonIncrementalStrategy;
import jetbrains.mps.generator.impl.dependencies.GenerationDependencies;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.messages.IMessage;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.MessageKind;
import jetbrains.mps.persistence.DefaultModelPersistence;
import jetbrains.mps.persistence.PersistenceUtil;
import jetbrains.mps.progress.EmptyProgressMonitor;
import jetbrains.mps.project.MPSExtentions;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.BaseMPSModuleOwner;
import jetbrains.mps.smodel.IOperationContext;
import jetbrains.mps.smodel.MPSModuleOwner;
import jetbrains.mps.testbench.PerformanceMessenger;
import jetbrains.mps.tool.environment.Environment;
import jetbrains.mps.tool.environment.EnvironmentConfig;
import jetbrains.mps.tool.environment.MpsEnvironment;
import jetbrains.mps.util.DifflibFacade;
import jetbrains.mps.util.FileUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.rules.TestName;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
/**
* @author Evgeny Gryaznov, Oct 6, 2010
*/
public class GenerationTestBase {
private static boolean DEBUG = false;
private final MPSModuleOwner myOwner = new BaseMPSModuleOwner();
private static Environment CREATED_ENV;
@ClassRule
public static final PerformanceMessenger ourStats = new PerformanceMessenger("Generator.");
@Rule
public final TestName myTestName = new TestName();
@BeforeClass
public static void init() throws Exception {
CREATED_ENV = MpsEnvironment.getOrCreate(EnvironmentConfig.defaultConfig());
}
@AfterClass
public static void clean() throws Exception {
if (CREATED_ENV != null) {
CREATED_ENV.release();
CREATED_ENV = null;
}
}
protected void doMeasureParallelGeneration(final Project p, final SModel descr, int threads) throws IOException {
final SRepository repo = p.getRepository();
final TestMessageHandler msg = new TestMessageHandler();
// Stage 1. Regenerate. Warm-up
GenerationOptions options = GenerationOptions.getDefaults()
.generateInParallel(false, 1)
.rebuildAll(true).strictMode(true).reporting(false, true, false, 2).incremental(new DefaultNonIncrementalStrategy()).create();
new GenerationFacade(repo, options).transients(new TransientModelsProvider(repo, null)).messages(msg).process(new EmptyProgressMonitor(), descr);
// Stage 2. Regenerate. Measure time.
options = GenerationOptions.getDefaults()
.generateInParallel(false, 1)
.rebuildAll(true).strictMode(true).reporting(false, true, false, 2).incremental(new DefaultNonIncrementalStrategy()).create();
long start = System.nanoTime();
new GenerationFacade(repo, options).transients(new TransientModelsProvider(repo, null)).messages(msg).process(new EmptyProgressMonitor(), descr);
long singleThread = System.nanoTime() - start;
// Stage 3. Regenerate in parallel
options = GenerationOptions.getDefaults()
.generateInParallel(true, threads)
.rebuildAll(true).strictMode(true).reporting(false, true, false, 2).incremental(new DefaultNonIncrementalStrategy()).create();
start = System.nanoTime();
new GenerationFacade(repo, options).transients(new TransientModelsProvider(repo, null)).messages(msg).process(new EmptyProgressMonitor(), descr);
long severalThreads = System.nanoTime() - start;
String prefix = myTestName.getMethodName();
ourStats.report(prefix + ".single", singleThread);
ourStats.report(prefix + ".parallel", severalThreads);
ourStats.reportPercent(prefix + ".parallelVsSingle", severalThreads / 1000000, singleThread / 1000000);
if (DEBUG) {
System.out.println("Single thread: " + singleThread / 1000000 / 1000. + ", 4 threads: " + severalThreads / 1000000 / 1000.);
}
}
protected void doTestIncrementalGeneration(final Project p, final SModel originalModel, final ModelChangeRunnable... changeModel) throws IOException {
String randomName = "testxw" + Math.abs(UUID.randomUUID().getLeastSignificantBits()) + "." + originalModel.getModule().getModuleName();
String randomId = UUID.randomUUID().toString();
final TestModule tm = new TestModule(randomName, randomId, originalModel.getModule());
final SRepositoryExt repo = (SRepositoryExt) p.getRepository();
repo.getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
repo.registerModule(tm, myOwner);
}
});
final SModel[] descr1 = new SModel[]{null};
try {
repo.getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
descr1[0] = tm.createModel(originalModel);
tm.publish(descr1[0]);
}
});
final SModel descr = descr1[0];
File generatorCaches = new File(PathManager.getSystemPath(), "mps-generator-test");
if (generatorCaches.exists()) {
Assert.assertTrue(FileUtil.delete(generatorCaches));
}
Assert.assertTrue("cannot create caches folder", generatorCaches.mkdir());
final MyIncrementalGenerationStrategy incrementalStrategy = new MyIncrementalGenerationStrategy(descr,
new FileBasedGenerationCacheContainer(generatorCaches));
repo.getModelAccess().runReadAction(new Runnable() {
@Override
public void run() {
incrementalStrategy.buildHash();
}
});
List<String> hashes = new ArrayList<String>();
hashes.add(incrementalStrategy.getHash().get(GeneratableSModel.FILE));
// Stage 1. Regenerate
GenerationOptions options = GenerationOptions.getDefaults()
.rebuildAll(true).strictMode(true).reporting(true, true, false, 2).incremental(incrementalStrategy).create();
IncrementalTestGenerationHandler generationHandler = new IncrementalTestGenerationHandler(repo);
GenerationFacade gf = new GenerationFacade(repo, options).messages(new TestMessageHandler()).transients(new TransientModelsProvider(repo, null));
GenerationStatus genStatus = gf.process(new EmptyProgressMonitor(), descr);
generationHandler.handleOutput(descr, genStatus);
Map<String, String> generated = replaceInContent(generationHandler.getGeneratedContent(),
new String[]{randomName, originalModel.getModule().getModuleName()},
new String[]{randomId, originalModel.getModule().getModuleReference().getModuleId().toString()});
assertNoDiff(generationHandler.getExistingContent(), generated);
// Stage 2. Modify model
Map<String, String> incrementalGenerationResults = generationHandler.getGeneratedContent();
List<Long> time = new ArrayList<Long>();
Assert.assertTrue(changeModel.length > 0);
for (final ModelChangeRunnable r : changeModel) {
ThreadUtils.runInUIThreadAndWait(new Runnable() {
@Override
public void run() {
repo.getModelAccess().executeCommand(new Runnable() {
@Override
public void run() {
r.run(descr);
}
});
}
});
repo.getModelAccess().runReadAction(new Runnable() {
@Override
public void run() {
incrementalStrategy.buildHash();
}
});
hashes.add(incrementalStrategy.getHash().get(GeneratableSModel.FILE));
Assert.assertNotNull(generationHandler.getLastDependencies());
incrementalStrategy.setDependencies(generationHandler.getLastDependencies());
// Stage 3. Generate incrementally
options = GenerationOptions.getDefaults()
.rebuildAll(false).strictMode(true).reporting(true, true, false, 2).incremental(incrementalStrategy).create();
generationHandler = new IncrementalTestGenerationHandler(repo, incrementalGenerationResults);
generationHandler.checkIncremental(options);
long start = System.nanoTime();
gf = new GenerationFacade(repo, options).messages(new TestMessageHandler()).transients(new TransientModelsProvider(repo, null));
genStatus = gf.process(new EmptyProgressMonitor(), descr);
generationHandler.handleOutput(descr, genStatus);
time.add(System.nanoTime() - start);
incrementalGenerationResults = generationHandler.getGeneratedContent();
assertDiff(generationHandler.getExistingContent(), incrementalGenerationResults, 1);
}
// Stage 4. Regenerate. Check incremental results.
incrementalStrategy.setDependencies(null);
options = GenerationOptions.getDefaults()
.rebuildAll(true).strictMode(true).reporting(true, true, false, 2).incremental(incrementalStrategy).create();
generationHandler = new IncrementalTestGenerationHandler(repo, incrementalGenerationResults);
long start = System.nanoTime();
gf = new GenerationFacade(repo, options).messages(new TestMessageHandler()).transients(new TransientModelsProvider(repo, null));
genStatus = gf.process(new EmptyProgressMonitor(), descr);
generationHandler.handleOutput(descr, genStatus);
time.add(System.nanoTime() - start);
assertNoDiff(incrementalGenerationResults, generationHandler.getGeneratedContent());
ourStats.reportPercent(myTestName.getMethodName() + ".incrementalGeneration", (time.get(time.size() - 2)) / 1000000,
(time.get(time.size() - 1)) / 1000000);
if (DEBUG) {
long regen = time.remove(time.size() - 1);
System.out.print("Full cycle: " + regen / 1000000 / 1000.);
for (long l : time) {
System.out.print(", incremental: " + l / 1000000 / 1000.);
}
System.out.println();
}
} finally {
repo.getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
repo.unregisterModule(tm, myOwner);
}
});
}
}
private Map<String, String> replaceInContent(Map<String, String> content, String[]... pairs) {
Map<String, String> result = new HashMap<String, String>(content.size());
for (Entry<String, String> e : content.entrySet()) {
String s = e.getValue();
for (String[] p : pairs) {
s = s.replaceAll(p[0], p[1]);
}
result.put(e.getKey(), s);
}
return result;
}
protected static SModel findModel(Project project, String fqName) {
for (SModule m : project.getProjectModules()) {
for (SModel descr : m.getModels()) {
if (!(descr instanceof EditableSModel)) {
continue;
}
if (fqName.equals(descr.getName().getValue())) {
return descr;
}
}
}
Assert.fail(fqName + " not found in " + project.getName());
return null;
}
protected static Project loadProject(File projectFile) {
return CREATED_ENV.openProject(projectFile);
}
protected static void cleanup(final Project p) {
p.dispose();
}
protected static void assertNoDiff(Map<String, String> expected, Map<String, String> actual) {
String errors = buildDiff(expected, actual);
if (errors.length() > 0) {
Assert.fail("Diff:\n" + errors);
}
}
private static Map<String, String> getHashes(SModel model) {
Map<String, String> rv = null;
try {
InputStream modelContents = PersistenceUtil.modelContentAsStream(model, MPSExtentions.MODEL);
final InputStreamReader reader = new InputStreamReader(modelContents, FileUtil.DEFAULT_CHARSET);
rv = DefaultModelPersistence.getDigestMap(reader);
reader.close();
} catch (IOException e) {
Assert.fail(e.getMessage());
}
return rv;
}
private static Map<String, String> getEmptyDigest() {
Map<String, String> result = new HashMap<String, String>();
result.put(GeneratableSModel.FILE, ModelDigestUtil.hashText(""));
result.put(GeneratableSModel.HEADER, ModelDigestUtil.hashText(""));
return result;
}
private static String buildDiff(Map<String, String> expected, Map<String, String> actual) {
Set<String> keys = new HashSet<String>();
keys.addAll(expected.keySet());
keys.addAll(actual.keySet());
StringBuilder errors = new StringBuilder();
for (String name : keys) {
String content = actual.get(name);
if (content == null) {
errors.append("File is not generated: " + name + "\n");
continue;
}
String existing = expected.get(name);
if (existing == null) {
errors.append("Non-existing file generated: " + name + /* "\nContent: " + content + */ "\n");
continue;
}
if (!existing.equals(content)) {
for (String s : DifflibFacade.getSimpleDiff(existing, content)) {
errors.append(s).append('\n');
}
}
}
return errors.toString();
}
protected void assertDiff(Map<String, String> expected, Map<String, String> actual, int numberOfChanges) {
if (DEBUG) {
System.out.println("Diff (debug):\n" + buildDiff(expected, actual));
}
Set<String> keys = new HashSet<String>();
keys.addAll(expected.keySet());
keys.addAll(actual.keySet());
int changes = 0;
for (String name : keys) {
String content = actual.get(name);
if (content == null) {
changes++;
continue;
}
String existing = expected.get(name);
if (existing == null) {
changes++;
continue;
}
if (!existing.equals(content)) {
changes++;
}
}
Assert.assertTrue("At least " + numberOfChanges + " are required (have " + changes + ")", changes >= numberOfChanges);
}
private static class MyIncrementalGenerationStrategy extends DefaultIncrementalStrategy {
private final SModel myModel;
private Map<String, String> myHash;
private GenerationDependencies myDependencies;
public MyIncrementalGenerationStrategy(SModel descr, FileBasedGenerationCacheContainer generationCacheContainer) {
super(generationCacheContainer);
myModel = descr;
}
void buildHash() {
Map<String, String> hashes = getHashes(myModel);
if (myHash != null) {
Assert.assertEquals("header's SHA1 shouldn't change after model change", myHash.get(GeneratableSModel.HEADER),
hashes.get(GeneratableSModel.HEADER));
Assert.assertNotSame("file's SHA1 should change after model change", myHash.get(GeneratableSModel.FILE), hashes.get(GeneratableSModel.FILE));
}
myHash = hashes;
}
public Map<String, String> getHash() {
return myHash;
}
@Override
public Map<String, String> getModelHashes(SModel sm, IOperationContext operationContext) {
if (sm == myModel) {
return myHash;
}
return getEmptyDigest(); // ModelDigestHelper.getInstance().getGenerationHashes(sm, operationContext);
}
@Override
public GenerationDependencies getDependencies(SModel sm) {
if (myModel != sm) {
return null;
}
return myDependencies;
}
public void setDependencies(GenerationDependencies dependencies) {
myDependencies = dependencies;
}
}
protected interface ModelChangeRunnable {
void run(SModel model);
}
private static class TestMessageHandler implements IMessageHandler {
@Override
public void handle(@NotNull IMessage msg) {
switch (msg.getKind()) {
case ERROR:
case WARNING:
Assert.fail((msg.getKind() == MessageKind.ERROR ? "error: " : "warning: ") + msg.getText() + msg.getException());
break;
case INFORMATION:
//System.out.println(msg.getText());
break;
}
}
}
}