diff --git a/src/org/rascalmpl/library/Prelude.java b/src/org/rascalmpl/library/Prelude.java index 1638ffe255e..beb072eb319 100644 --- a/src/org/rascalmpl/library/Prelude.java +++ b/src/org/rascalmpl/library/Prelude.java @@ -3784,6 +3784,7 @@ public void sleep(IInteger seconds) { private static final class ReleasableCallback implements Consumer { private final WeakReference target; private final ISourceLocation src; + private final ISourceLocation srcResolved; private final boolean recursive; private final int hash; @@ -3792,6 +3793,7 @@ private static final class ReleasableCallback implements Consumer(target); this.hash = src.hashCode() + 7 * target.hashCode(); @@ -3799,8 +3801,30 @@ public ReleasableCallback(ISourceLocation src, boolean recursive, IFunction targ this.store = store; } + private static ISourceLocation safeResolve(ISourceLocation src) { + try { + var result = URIResolverRegistry.getInstance().logicalToPhysical(src); + if (result != null) { + return result; + } + return src; + } catch (IOException e) { + return src; + } + } + + private boolean exactMatch(ISourceLocation loc) { + return loc.equals(src) || (srcResolved != src && loc.equals(srcResolved)); + } + @Override public void accept(ISourceLocationChanged e) { + if (!recursive && !exactMatch(e.getLocation())) { + // if we are not recursive, and changes come in for something that's not what we requested + // for example due to the backend only supporting directory level watches + // we just ignore it + return; + } IFunction callback = target.get(); if (callback == null) { try { diff --git a/src/org/rascalmpl/test/infrastructure/RecursiveTestSuite.java b/src/org/rascalmpl/test/infrastructure/RecursiveTestSuite.java index 63b3d65b5cc..712e24a966b 100644 --- a/src/org/rascalmpl/test/infrastructure/RecursiveTestSuite.java +++ b/src/org/rascalmpl/test/infrastructure/RecursiveTestSuite.java @@ -20,6 +20,8 @@ import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; +import junit.framework.TestCase; + public class RecursiveTestSuite extends Suite { public RecursiveTestSuite(Class setupClass) @@ -68,6 +70,9 @@ else if (f.getName().endsWith(".class")) { result.add(currentClass); } } + else if (TestCase.class.isAssignableFrom(currentClass)) { + result.add(currentClass); + } else { for (Method m: currentClass.getMethods()) { if (m.isAnnotationPresent(Test.class)) { diff --git a/test/org/rascalmpl/test/functionality/IOTests.java b/test/org/rascalmpl/test/functionality/IOTests.java index ebef897f48b..c7840fc8a5a 100644 --- a/test/org/rascalmpl/test/functionality/IOTests.java +++ b/test/org/rascalmpl/test/functionality/IOTests.java @@ -17,6 +17,16 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.IEvaluator; +import org.rascalmpl.interpreter.env.GlobalEnvironment; +import org.rascalmpl.interpreter.env.ModuleEnvironment; +import org.rascalmpl.interpreter.load.StandardLibraryContributor; +import org.rascalmpl.interpreter.result.Result; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.ValueFactoryFactory; + +import io.usethesource.vallang.IBool; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.exceptions.FactTypeUseException; @@ -24,8 +34,6 @@ import io.usethesource.vallang.type.Type; import io.usethesource.vallang.type.TypeFactory; import io.usethesource.vallang.type.TypeStore; -import org.rascalmpl.values.ValueFactoryFactory; - import junit.framework.TestCase; public class IOTests extends TestCase { @@ -105,5 +113,101 @@ public void testATermReader() { } } + private final IEvaluator> setupWatchEvaluator() { + return setupWatchEvaluator(false); + } + private final IEvaluator> setupWatchEvaluator(boolean debug) { + var heap = new GlobalEnvironment(); + var root = heap.addModule(new ModuleEnvironment("___test___", heap)); + var evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, System.err, System.out, root, heap); + + evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); + + evaluator.addRascalSearchPath(URIUtil.rootLocation("test-modules")); + evaluator.addRascalSearchPath(URIUtil.rootLocation("benchmarks")); + executeCommand(evaluator, "import IO;"); + executeCommand(evaluator, "int trig = 0;"); + executeCommand(evaluator, "void triggerWatch(LocationChangeEvent tp) { trig = trig + 1; " + (debug? "println(tp);": "") + " }"); + return evaluator; + } + + private static IValue executeCommand(IEvaluator> eval, String command) { + var result = eval.eval(null, command, URIUtil.rootLocation("stdin")); + if (result.getStaticType().isBottom()) { + return null; + } + return result.getValue(); + } + + private static boolean executeBooleanExpression(IEvaluator> eval, String expr) { + var result = executeCommand(eval, expr); + if (result instanceof IBool) { + return ((IBool)result).getValue(); + } + return false; + } + + + public void testWatch() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "writeFile(|tmp:///a/make-dir.txt|, \"hi\");"); + executeCommand(evalTest, "watch(|tmp:///a/|, true, triggerWatch);"); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + Thread.sleep(100); // give it some time to trigger the watch callback + + assertTrue("Watch should have been triggered", executeBooleanExpression(evalTest, "trig > 0")); + } + + public void testWatchNonRecursive() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "watch(|tmp:///a/test-watch.txt|, false, triggerWatch);"); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + Thread.sleep(100); // give it some time to trigger the watch callback + assertTrue("Watch should have been triggered", executeBooleanExpression(evalTest, "trig > 0")); + } + + public void testWatchDelete() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + executeCommand(evalTest, "watch(|tmp:///a/|, true, triggerWatch);"); + executeCommand(evalTest, "remove(|tmp:///a/test-watch.txt|);"); + Thread.sleep(100); // give it some time to trigger the watch callback + assertTrue("Watch should have been triggered for delete", executeBooleanExpression(evalTest, "trig > 0")); + } + + public void testWatchSingleFile() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch-a.txt|, \"making it exist\");"); + executeCommand(evalTest, "watch(|tmp:///a/test-watch-a.txt|, false, triggerWatch);"); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"bye\");"); + executeCommand(evalTest, "remove(|tmp:///a/test-watch.txt|);"); + Thread.sleep(100); // give it some time to trigger the watch callback + assertTrue("Watch should not have triggered anything", executeBooleanExpression(evalTest, "trig == 0")); + } + + public void testUnwatchStopsEvents() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "watch(|tmp:///a/|, true, triggerWatch);"); + Thread.sleep(10); + executeCommand(evalTest, "unwatch(|tmp:///a/|, true, triggerWatch);"); + Thread.sleep(100); // give it some time to trigger the watch callback + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + executeCommand(evalTest, "remove(|tmp:///a/test-watch.txt|);"); + Thread.sleep(100); // give it some time to trigger the watch callback + assertTrue("Watch should not have triggered anything", executeBooleanExpression(evalTest, "trig == 0")); + } + + public void testUnwatchStopsEventsUnrecursive() throws InterruptedException { + var evalTest = setupWatchEvaluator(); + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + executeCommand(evalTest, "watch(|tmp:///a/test-watch.txt|, false, triggerWatch);"); + Thread.sleep(10); + executeCommand(evalTest, "unwatch(|tmp:///a/test-watch.txt|, false, triggerWatch);"); + Thread.sleep(100); // give it some time to trigger the watch callback + executeCommand(evalTest, "writeFile(|tmp:///a/test-watch.txt|, \"hi\");"); + executeCommand(evalTest, "remove(|tmp:///a/test-watch.txt|);"); + Thread.sleep(100); // give it some time to trigger the watch callback + assertTrue("Watch should not have triggered anything", executeBooleanExpression(evalTest, "trig == 0")); + } }