From e8945fe1f0c9bf4f5732882b4550e2ce7b7468cd Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Sat, 13 Jun 2015 21:01:38 -0700 Subject: [PATCH] add support for testing functions with target --- liquid-server.cabal | 2 +- resources/custom/liquidhaskell/config.json | 1 + resources/custom/nanojs/config.json | 1 + resources/static/index.html | 2 + resources/static/js/liquid.js | 138 +++++++++++++-------- src/Language/Liquid/Server/Query.hs | 40 +++++- src/Language/Liquid/Server/Scotty.hs | 2 +- src/Language/Liquid/Server/Types.hs | 16 ++- 8 files changed, 138 insertions(+), 64 deletions(-) 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"