diff --git a/liquid-server.cabal b/liquid-server.cabal
index 1306336..c55ea7c 100644
--- a/liquid-server.cabal
+++ b/liquid-server.cabal
@@ -26,7 +26,7 @@ Executable liquid-server
snap-core >= 0.9 && < 0.11,
snap-server >= 0.9 && < 0.11,
aeson,
- hashable < 1.2,
+ hashable,
unordered-containers,
time,
process,
diff --git a/resources/custom/liquidhaskell/config.json b/resources/custom/liquidhaskell/config.json
index ad23719..fb09350 100644
--- a/resources/custom/liquidhaskell/config.json
+++ b/resources/custom/liquidhaskell/config.json
@@ -6,4 +6,5 @@
, "modeFile" : "mode-haskell.js"
, "tmpDir" : ".liquid"
, "port" : 8090
+, "srcTester" : "target"
}
diff --git a/resources/custom/nanojs/config.json b/resources/custom/nanojs/config.json
index 4908cc2..5b497f4 100644
--- a/resources/custom/nanojs/config.json
+++ b/resources/custom/nanojs/config.json
@@ -5,4 +5,5 @@
, "themeFile" : "theme-xcode.js"
, "modeFile" : "mode-javascript.js"
, "tmpDir" : ""
+, "srcTester" : ""
}
diff --git a/resources/static/index.html b/resources/static/index.html
index 58951d4..83e2850 100644
--- a/resources/static/index.html
+++ b/resources/static/index.html
@@ -191,6 +191,8 @@
{{demoTitle}}
+
+
diff --git a/resources/static/js/liquid.js b/resources/static/js/liquid.js
index 2257f3e..082ac33 100644
--- a/resources/static/js/liquid.js
+++ b/resources/static/js/liquid.js
@@ -12,10 +12,10 @@ function getDemo(name){
return res;
}
-function getDemos(ty){
+function getDemos(ty){
var a = [];
- for (var k in allDemos) {
- if (allDemos[k].type == ty)
+ for (var k in allDemos) {
+ if (allDemos[k].type == ty)
a.push(getDemo(k));
};
return a;
@@ -32,7 +32,7 @@ function getCategories(){
function tx(c){return {type: c.type, name: c.name, demos: getDemos(c.type)}};
return allCategories.map(tx);
}
-
+
/*******************************************************************************/
/************** Setting Up Editor **********************************************/
@@ -62,7 +62,7 @@ function toggleEditorSize(x){
if (x.isFullScreen){
ht = 600;
};
- $("#program-pane").height(ht);
+ $("#program-pane").height(ht);
$("#program").height(ht-60);
}
@@ -73,12 +73,12 @@ function toggleEditorSize(x){
/*******************************************************************************/
function errorRange(err){
-
+
var row0 = err.start.line - 1;
var col0 = err.start.column - 1;
var row1 = err.stop.line - 1;
var col1 = err.stop.column - 1;
-
+
if (row0 == row1 && col0 == col1){
return new Range(row0, col0, row0, col0 + 1);
} else {
@@ -109,7 +109,7 @@ function setErrors(editor, errs){
// Add Error Markers
errorMarkers.forEach(function(m){ editor.session.removeMarker(m); });
errorMarkers = errs.map(function(e){ return errorMarker(editor, e);});
-
+
// Add Gutter Annotations
editor.session.clearAnnotations();
var annotations = errs.map(errorAceAnnot);
@@ -121,18 +121,18 @@ function setErrors(editor, errs){
/************** URLS ***********************************************************/
/*******************************************************************************/
-function isPrefix(p, q) {
- return (p == q.slice(0, p.length))
+function isPrefix(p, q) {
+ return (p == q.slice(0, p.length))
}
-function getQueryURL(){
- return 'query';
+function getQueryURL(){
+ return 'query';
}
-function getSrcURL(file){
+function getSrcURL(file){
if (file.match("/")){
return file;
- } else {
+ } else {
return ('demos/' + file);
}
}
@@ -143,9 +143,9 @@ function getSrcURL(file){
/************** Queries ********************************************************/
/*******************************************************************************/
-function getCheckQuery($scope){
+function getCheckQuery($scope){
return { type : "check",
- program : getSourceCode()
+ program : getSourceCode()
};
}
@@ -153,14 +153,21 @@ function getRecheckQuery($scope){
var p = "";
if ($scope.filePath) p = $scope.filePath;
- return { type : "recheck",
- program : getSourceCode(),
+ return { type : "recheck",
+ program : getSourceCode(),
path : p
};
}
+function getTestQuery($scope){
+ return { type : "test",
+ program : getSourceCode(),
+ binder : getBinder()
+ };
+}
+
function getLoadQuery($scope){
- return { type : "load",
+ return { type : "load",
path : $scope.localFilePath
};
}
@@ -213,15 +220,19 @@ function setStatusResult($scope, data){
function setSourceCode($scope, srcName, srcText){
clearStatus($scope);
- $scope.filePath = null;
+ $scope.filePath = null;
$scope.sourceFileName = srcName.split("/").pop(); // drop path prefix
- progEditor.getSession().setValue(srcText);
+ progEditor.getSession().setValue(srcText);
}
function getSourceCode(){
return progEditor.getSession().getValue();
}
+function getBinder(){
+ return document.getElementById("binder").value;
+}
+
/*******************************************************************************/
/************** Loading Files **************************************************/
/*******************************************************************************/
@@ -240,10 +251,10 @@ function fileText(file, k){
function loadLocalFile($scope, file){
if (window.File && window.FileList && window.FileReader && file) {
if (file.type.match('text')) {
- fileText(file, function(srcText){
- setSourceCode($scope, file.name, srcText);
+ fileText(file, function(srcText){
+ setSourceCode($scope, file.name, srcText);
});
- } else {
+ } else {
alert("Can only load text files.");
}
} else {
@@ -256,18 +267,18 @@ function loadLocalFile($scope, file){
/** Extracting JSON Results ****************************************************/
/*******************************************************************************/
-function getResult(d) {
+function getResult(d) {
var res = "crash";
if (d) {
- res = d.status;
+ res = d.status;
}
return res;
}
-function getWarns(d){
+function getWarns(d){
var ws = [];
if (d && d.errors){
- var ws = d.errors.map(function(x){
+ var ws = d.errors.map(function(x){
return x.message;
});
}
@@ -290,7 +301,7 @@ var debugZ = null;
function LiquidDemoCtrl($scope, $http, $location) {
// Start in non-fullscreen
- $scope.isFullScreen = false;
+ $scope.isFullScreen = false;
$scope.embiggen = "FullScreen";
$scope.demoTitle = demoTitle;
$scope.demoSubtitle = demoSubtitle;
@@ -310,54 +321,54 @@ function LiquidDemoCtrl($scope, $http, $location) {
};
// LOAD a file from disk (only when isLocalServer)
- $scope.loadFromLocalPath = function(){
+ $scope.loadFromLocalPath = function(){
var srcName = $scope.localFilePath;
if (srcName){
- // alert('so you want to load' + $scope.localFilePath);
+ // alert('so you want to load' + $scope.localFilePath);
$http.post(getQueryURL(), getLoadQuery($scope))
.success(function(data, status){
debugData = data;
- if (data.program) {
+ if (data.program) {
setSourceCode($scope, srcName, data.program);
} else if (data.error) {
- alert("Load Error " + data.error);
+ alert("Load Error " + data.error);
} else {
- alert("Horrors: Load Failed! " + srcName);
+ alert("Horrors: Load Failed! " + srcName);
}
})
.error(function(data, status){
- alert("Load Error: No response for " + srcName);
+ alert("Load Error: No response for " + srcName);
});
}
};
// SAVE a file to disk (only when isLocalServer)
- $scope.saveToLocalPath = function(){
+ $scope.saveToLocalPath = function(){
var srcName = $scope.localFilePath;
- //alert('so you want to save ' + $scope.localFilePath);
+ //alert('so you want to save ' + $scope.localFilePath);
if (srcName) {
$http.post(getQueryURL(), getSaveQuery($scope))
.success(function(data, status){
debugData = data;
if (data.path){
- alert("Saved.");
+ alert("Saved.");
} else {
alert("Save Unsuccessful: " + data);
}
})
.error(function(data, status){
- alert("Save Failed: " + data);
+ alert("Save Failed: " + data);
});
}
};
// Clear Status when editor is changed
- progEditor.on("change", function(e){
+ progEditor.on("change", function(e){
$scope.$apply(function(){
clearStatus($scope);
});
});
-
+
// Load a particular demo
$scope.loadSource = function(demo){
@@ -375,7 +386,7 @@ function LiquidDemoCtrl($scope, $http, $location) {
debugDemo = getDefaultDemo();
$scope.loadSource(debugDemo); //getDefaultDemo());
- // Extract demo name from URL
+ // Extract demo name from URL
$scope.$watch('location.search()', function() {
// debugZ = ($location.search()).demo;
$scope.demoName = ($location.search()).demo;
@@ -386,12 +397,12 @@ function LiquidDemoCtrl($scope, $http, $location) {
$scope.loadSource(newDemo);
}, true);
- // Update demo name in URL
+ // Update demo name in URL
$scope.changeTarget = function(demo) {
$location.search('demo', demo.file);
$scope.loadSource(demo);
};
-
+
// Change editor keybindings
$scope.keyBindingsNone = function (){ progEditor.setKeyboardHandler(null); };
$scope.keyBindingsVim = function (){ progEditor.setKeyboardHandler("ace/keyboard/vim"); };
@@ -405,44 +416,63 @@ function LiquidDemoCtrl($scope, $http, $location) {
debugData = data;
$scope.changeTarget({file : data.path});
} else {
- alert("Permalink did not return link: " + data);
+ alert("Permalink did not return link: " + data);
}
})
.error(function(data, status){
- alert("Permalink Failed: " + status);
+ alert("Permalink Failed: " + status);
});
};
// http://www.cleverweb.nl/javascript/a-simple-search-with-angularjs-and-php/
- function verifyQuery(query){
+ function verifyQuery(query){
debugQuery = query;
setStatusChecking($scope);
$http.post(getQueryURL(), query)
.success(function(data, status) {
- debugResp = debugResp + 1;
+ debugResp = debugResp + 1;
$scope.status = status;
debugData = data;
- $scope.warns = getWarns(data);
+ $scope.warns = getWarns(data);
$scope.annotHtml = data.annotHtml;
$scope.result = setStatusResult($scope, data);
-
+
// This may be "null" if liquid crashed...
- if (data) {
+ if (data) {
setAnnots(data.types);
setErrors(progEditor, data.errors);
};
-
+
})
.error(function(data, status) {
var msg = (data || "Request failed") + status;
alert(msg);
});
};
-
+
+ function testQuery(query){
+ debugQuery = query;
+ setStatusChecking($scope);
+ $http.post(getQueryURL(), query)
+ .success(function(data, status) {
+ debugResp = debugResp + 1;
+ $scope.status = status;
+ debugData = data;
+ $scope.result = setStatusResult($scope, data);
+ $scope.warns = [data.message];
+
+ })
+ .error(function(data, status) {
+ var msg = (data || "Request failed") + status;
+ alert(msg);
+ });
+ };
+
$scope.verifySource = function(){ verifyQuery(getCheckQuery($scope)); };
$scope.reVerifySource = function(){ verifyQuery(getRecheckQuery($scope)); };
-
+ $scope.testSource = function(){ testQuery(getTestQuery($scope)); };
+
}
/************************************************************************/
diff --git a/src/Language/Liquid/Server/Query.hs b/src/Language/Liquid/Server/Query.hs
index 2d54711..2c4a74c 100644
--- a/src/Language/Liquid/Server/Query.hs
+++ b/src/Language/Liquid/Server/Query.hs
@@ -4,10 +4,10 @@ module Language.Liquid.Server.Query (queryResult) where
import System.IO.Error (catchIOError)
-import System.Exit (ExitCode)
-import System.Directory (doesFileExist)
+import System.Exit (ExitCode(..))
+import System.Directory (doesFileExist, findExecutable)
import System.FilePath ((>), addExtension, splitFileName)
-import System.Process (system)
+import System.Process (readProcessWithExitCode, system)
import Control.Applicative ((<$>))
import Control.Exception (throw)
import Data.Maybe
@@ -28,6 +28,7 @@ queryResult :: Config -> Ticket -> Query -> IO Result
---------------------------------------------------------------
queryResult c t q@(Check {}) = checkResult c t q
queryResult c _ q@(Recheck {}) = recheckResult c q
+queryResult c t q@(Test {}) = testResult c t q
queryResult _ _ q@(Load {}) = loadResult q
queryResult _ _ q@(Save {}) = saveResult q
queryResult c t q@(Perma {}) = permaResult c t q
@@ -116,6 +117,32 @@ execCheck c f
r <- readResult f
return $ r += ("path", toJSON $ srcFile f)
+
+---------------------------------------------------------------
+testResult :: Config -> Ticket -> Query -> IO Result
+---------------------------------------------------------------
+testResult c t q = genFiles c t >>= writeQuery q >>= execTest c q
+
+
+---------------------------------------------------------------
+execTest :: Config -> Query -> Files -> IO Result
+---------------------------------------------------------------
+execTest c q f
+ = do Just bin <- findExecutable (srcTester c)
+ (x,o,e) <- readProcessWithExitCode
+ bin [srcFile f, T.unpack (binder q)] ""
+ print o
+ print e
+ writeFile (logFile c) (o ++ "\n" ++ e)
+ let r = case x of
+ ExitSuccess -> mkResult [ ("status", "safe") ]
+ ExitFailure 1 -> mkResult [ ("status", "unsafe")
+ , ("message", T.pack o)]
+ ExitFailure 2 -> errResult (T.pack e)
+ print r
+ return $ r += ("path", toJSON $ srcFile f)
+
+
---------------------------------------------------------------
writeQuery :: Query -> Files -> IO Files
---------------------------------------------------------------
@@ -141,7 +168,7 @@ readResult f = do b <- doesFileExist file
---------------------------------------------------------------
makeCommand :: Config -> FilePath -> String
---------------------------------------------------------------
-makeCommand config t = intercalate " "
+makeCommand config t = unwords
[ cmdPrefix config
, srcChecker config
, t
@@ -150,6 +177,11 @@ makeCommand config t = intercalate " "
, "2>&1"
]
+
+makeTestCommand :: Config -> FilePath -> T.Text -> String
+makeTestCommand config t bnd
+ = unwords [ srcTester config, t, show bnd, ">", logFile config, "2>&1" ]
+
---------------------------------------------------------------
-- | Redirecting Custom Files ---------------------------------
---------------------------------------------------------------
diff --git a/src/Language/Liquid/Server/Scotty.hs b/src/Language/Liquid/Server/Scotty.hs
index a936be1..b8272c6 100644
--- a/src/Language/Liquid/Server/Scotty.hs
+++ b/src/Language/Liquid/Server/Scotty.hs
@@ -56,7 +56,7 @@ site cfg t = route
, (get "/config.js" , serveFile $ configPath cfg )
, (get "/theme.js" , serveFile $ themePath cfg )
, (get "/mode.js" , serveFile $ modePath cfg )
- , (post "/query" , queryH cfg t )
+ , (post "/query" , queryH cfg t )
, (get "/log" , serveFileAsText $ logFile cfg )
, (get "/demos/:path" , serveFileAt $ demoPath cfg )
, (get "/permalink/:path" , serveFileAt $ sandboxPath cfg )
diff --git a/src/Language/Liquid/Server/Types.hs b/src/Language/Liquid/Server/Types.hs
index 592fab9..1d394f2 100644
--- a/src/Language/Liquid/Server/Types.hs
+++ b/src/Language/Liquid/Server/Types.hs
@@ -12,7 +12,7 @@ module Language.Liquid.Server.Types (
, Result
-- * Canned Responses
- , dummyResult, okResult, errResult
+ , dummyResult, okResult, errResult, mkResult
) where
import Control.Monad (mzero)
@@ -29,12 +29,13 @@ import qualified Data.HashMap.Strict as M
data Config = Config {
toolName :: String -- used to lookup resources/custom/toolName
, srcSuffix :: String -- hs, js etc.
- , srcChecker :: FilePath -- checker binary; must be in your $PATH
+ , srcChecker :: FilePath -- checker binary; must be in your $PATH
, cmdPrefix :: String -- extra command line params to be passed to `srcChecker`
, themeFile :: FilePath -- theme-THEMEFILE.js
, modeFile :: FilePath -- mode-MODEFILE.js
, tmpDir :: FilePath -- temp directory offset for files generated by checker
, port :: Int -- port at which to run server
+ , srcTester :: FilePath -- testing binary; must be in $PATH
} deriving (Show)
data Files = Files {
@@ -49,11 +50,14 @@ data Files = Files {
data Query = Check { program :: T.Text }
| Recheck { program :: T.Text
, path :: FilePath }
+ | Test { program :: T.Text
+ , binder :: T.Text }
| Save { program :: T.Text
, path :: FilePath }
| Load { path :: FilePath }
| Perma { program :: T.Text }
| Junk
+ deriving (Show)
type Result = Value
@@ -73,7 +77,8 @@ objectConfig v = Config <$> v .: "toolName"
<*> v .: "modeFile"
<*> v .: "tmpDir"
<*> v .: "port"
-
+ <*> v .: "srcTester"
+
----------------------------------------------------------------
-- JSON Serialization: Query -----------------------------------
----------------------------------------------------------------
@@ -87,7 +92,8 @@ objectQuery v
= do ty <- v .: "type"
case ty :: String of
"check" -> Check <$> v .: "program"
- "recheck" -> Recheck <$> v .: "program" <*> v.: "path"
+ "recheck" -> Recheck <$> v .: "program" <*> v.: "path"
+ "test" -> Test <$> v .: "program" <*> v.: "binder"
"perma" -> Perma <$> v .: "program"
"save" -> Save <$> v .: "program" <*> v.: "path"
"load" -> Load <$> v .: "path"
@@ -96,6 +102,7 @@ objectQuery v
instance ToJSON Query where
toJSON q@(Check prg) = object ["type" .= jsonType q, "program" .= prg]
toJSON q@(Recheck prg pth) = object ["type" .= jsonType q, "program" .= prg, "path" .= pth]
+ toJSON q@(Test prg bnd) = object ["type" .= jsonType q, "program" .= prg, "binder" .= bnd]
toJSON q@(Perma prg) = object ["type" .= jsonType q, "program" .= prg]
toJSON q@(Save prg pth) = object ["type" .= jsonType q, "program" .= prg, "path" .= pth]
toJSON q@(Load pth) = object ["type" .= jsonType q, "path" .= pth]
@@ -104,6 +111,7 @@ instance ToJSON Query where
jsonType :: Query -> String
jsonType (Check {}) = "check"
jsonType (Recheck {}) = "recheck"
+jsonType (Test {}) = "test"
jsonType (Perma {}) = "perma"
jsonType (Save {}) = "save"
jsonType (Load {}) = "load"