diff --git a/.circleci/config.yml b/.circleci/config.yml index 5951c68a8..631587302 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,6 +27,7 @@ jobs: name: Install Package Dependencies command: | sudo apt-get update + sudo apt-get install -y graphviz sudo apt-get install -y texlive-latex-base sudo wget https://imagemagick.org/archive/binaries/magick sudo cp magick /usr/local/bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da235807..b0b0dbbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### 🐛 Bug fixes - Fixed a bug that was causing the focus info popup to appear blank +- Fixed a bug on the generate page causing extraneous ellipses to appear when hovering over a course to highlight its prerequisites ### 🔧 Internal changes diff --git a/app/Svg/Parser.hs b/app/Svg/Parser.hs index e3f915d89..c57a146de 100644 --- a/app/Svg/Parser.hs +++ b/app/Svg/Parser.hs @@ -151,10 +151,12 @@ parseGraph key tags = small shape = shapeWidth shape < 300 removeRedundant shapes = filter (not . \s -> shapePos s `elem` map shapePos shapes && - (T.null (shapeFill s) || shapeFill s == "#000000") && + isEdge s && elem (shapeType_ s) [Node, Hybrid]) shapes - +-- | Determine if the input shape is an edge. +isEdge :: Shape -> Bool +isEdge shape = T.null (shapeFill shape) || shapeFill shape == "black" || shapeFill shape == "#000000" -- | Create text values from g tags. -- This searches for nested tspan tags inside text tags using a recursive diff --git a/backend-test/Controllers/GenerateControllerTests.hs b/backend-test/Controllers/GenerateControllerTests.hs new file mode 100644 index 000000000..773c0a648 --- /dev/null +++ b/backend-test/Controllers/GenerateControllerTests.hs @@ -0,0 +1,70 @@ +{-| +Description: Generate Controller module tests. + +Module that contains the tests for the functions in the Generate Controller module. + +-} + +module Controllers.GenerateControllerTests +( test_generateController +) where + +import Config (runDb) +import Controllers.Generate (findAndSavePrereqsResponse) +import Data.Aeson (Value (..), decode) +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as BSL +import Data.Foldable (toList) +import qualified Data.Text as T +import Database.Persist.Sqlite (SqlPersistM, insert_) +import Database.Tables (Courses (..)) +import Happstack.Server (rsBody) +import Test.Tasty (TestTree) +import Test.Tasty.HUnit (assertEqual, testCase) +import TestHelpers (clearDatabase, runServerPartWithGraphGenerate, withDatabase) + +-- | Helper function to insert courses into the database +insertCoursesWithPrerequisites :: [(T.Text, Maybe T.Text)] -> SqlPersistM () +insertCoursesWithPrerequisites = mapM_ insertCourse + where + insertCourse (code, prereqString) = insert_ (Courses code Nothing Nothing Nothing prereqString Nothing Nothing Nothing Nothing []) + +-- | List of test cases as (input course, course/prereq structure, JSON payload, expected # of nodes in prereq graph) +findAndSavePrereqsResponseTestCases :: [(String, [(T.Text, Maybe T.Text)], BSL.ByteString, Integer)] +findAndSavePrereqsResponseTestCases = + [("CSC148H1", + [("CSC108H1", Nothing), ("CSC148H1", Just "CSC108H1")], + "{\"courses\":[\"CSC148H1\"],\"programs\":[],\"graphOptions\":{\"taken\":[],\"departments\":[\"CSC\",\"MAT\",\"STA\"]}}", + 2 + )] + +-- | Run a test case (input course, course/prereq structure, JSON payload, expected # of nodes) on the findAndSavePrereqsResponse function. +runfindAndSavePrereqsResponseTest :: String -> [(T.Text, Maybe T.Text)] -> BSL.ByteString -> Integer -> TestTree +runfindAndSavePrereqsResponseTest course graphStructure payload expected = + testCase course $ do + runDb $ do + clearDatabase + insertCoursesWithPrerequisites graphStructure + response <- runServerPartWithGraphGenerate Controllers.Generate.findAndSavePrereqsResponse payload + -- Take the response and extract the number of nodes (courses) within the generated graph, then assert that it is equal to the expected value. + let body = rsBody response + Just (Object object) = decode body + Just (Array shapes) = KM.lookup (K.fromString "shapes") object + actual = + fromIntegral . length $ filter isNode (toList shapes) + + assertEqual ("Unexpected response for " ++ course) expected actual + + where + isNode (Object object) = + KM.lookup (K.fromString "type_") object == Just (String "Node") + isNode _ = False + +-- | Run all the findAndSavePrereqsResponse test cases +runfindAndSavePrereqsResponseTests :: [TestTree] +runfindAndSavePrereqsResponseTests = map (\(course, courseStructure, payload, expectedNodes) -> runfindAndSavePrereqsResponseTest course courseStructure payload expectedNodes) findAndSavePrereqsResponseTestCases + +-- | Test suite for Generate Controller Module +test_generateController :: TestTree +test_generateController = withDatabase "Generate Controller tests" runfindAndSavePrereqsResponseTests diff --git a/backend-test/TestHelpers.hs b/backend-test/TestHelpers.hs index 7633ef911..ae944174b 100644 --- a/backend-test/TestHelpers.hs +++ b/backend-test/TestHelpers.hs @@ -13,20 +13,22 @@ module TestHelpers releaseDatabase, runServerPartWithQuery, runServerPartWithCourseInfoQuery, + runServerPartWithGraphGenerate, withDatabase) where import Config (databasePath) -import Control.Concurrent.MVar (newEmptyMVar, newMVar) -import qualified Data.ByteString.Lazy.Char8 as BSL +import Control.Concurrent.MVar (newEmptyMVar, newMVar, putMVar) +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy.Char8 as BSL8 import qualified Data.Map as Map import Data.Text (unpack) import Database.Database (setupDatabase) import Database.Persist.Sqlite (Filter, SqlPersistM, deleteWhere) import Database.Tables -import Happstack.Server (ContentType (..), HttpVersion (..), Input (..), Method (GET), Request (..), - Response, ServerPart, inputContentType, inputFilename, inputValue, - simpleHTTP'') +import Happstack.Server (ContentType (..), HttpVersion (..), Input (..), Method (GET, PUT), + Request (..), Response, RqBody (..), ServerPart, inputContentType, + inputFilename, inputValue, simpleHTTP'') import System.Directory (removeFile) import System.Environment (setEnv, unsetEnv) import Test.Tasty (TestTree, testGroup, withResource) @@ -69,7 +71,7 @@ mockRequestWithQuery courseName = do , rqUri = "/course" , rqQuery = "" , rqInputsQuery = [("name", Input { - inputValue = Right (BSL.pack courseName), + inputValue = Right (BSL8.pack courseName), inputFilename = Nothing, inputContentType = defaultContentType })] @@ -93,7 +95,7 @@ mockRequestWithCourseInfoQuery dept = do , rqUri = "/course-info" , rqQuery = "" , rqInputsQuery = [("dept", Input { - inputValue = Right (BSL.pack dept), + inputValue = Right (BSL8.pack dept), inputFilename = Nothing, inputContentType = defaultContentType })] @@ -105,6 +107,28 @@ mockRequestWithCourseInfoQuery dept = do , rqPeer = ("127.0.0.1", 0) } +-- | A mock request for the graph generate route, specifically for findAndSavePrereqsResponse +mockRequestWithGraphGenerate :: BSL.ByteString -> IO Request +mockRequestWithGraphGenerate payload = do + inputsBody <- newMVar [] + requestBody <- newEmptyMVar + putMVar requestBody (Body payload) + + return Request + { rqSecure = False + , rqMethod = PUT + , rqPaths = ["graph-generate"] + , rqUri = "/graph-generate" + , rqQuery = "" + , rqInputsQuery = [] + , rqInputsBody = inputsBody + , rqCookies = [] + , rqVersion = HttpVersion 1 1 + , rqHeaders = Map.empty + , rqBody = requestBody + , rqPeer = ("127.0.0.1", 0) + } + -- | Default content type for the MockRequestWithQuery, specifically for retrieveCourse defaultContentType :: ContentType defaultContentType = ContentType @@ -125,6 +149,12 @@ runServerPartWithCourseInfoQuery sp dept = do request <- mockRequestWithCourseInfoQuery dept simpleHTTP'' sp request +-- | Helper function to run ServerPartWithGraphGenerate for findAndSavePrereqsResponse +runServerPartWithGraphGenerate :: ServerPart Response -> BSL.ByteString -> IO Response +runServerPartWithGraphGenerate sp payload = do + request <- mockRequestWithGraphGenerate payload + simpleHTTP'' sp request + -- | Clear all the entries in the database clearDatabase :: SqlPersistM () clearDatabase = do diff --git a/courseography.cabal b/courseography.cabal index a06ff8cc6..ae66d9512 100644 --- a/courseography.cabal +++ b/courseography.cabal @@ -24,6 +24,7 @@ library exposed-modules: Config, Controllers.Course, + Controllers.Generate, Controllers.Graph, Css.Constants, Database.CourseInsertion, @@ -33,7 +34,11 @@ library Database.DataType, Database.Requirement, Database.Tables, + DynamicGraphs.CourseFinder, + DynamicGraphs.GraphGenerator, DynamicGraphs.GraphNodeUtils, + DynamicGraphs.GraphOptions, + DynamicGraphs.WriteRunDot, Export.GetImages, Export.ImageConversion, Export.TimetableImageCreator, @@ -68,6 +73,8 @@ library directory, diagrams-lib, diagrams-svg, + filepath, + graphviz, happstack-server, hlint, hslogger, @@ -105,6 +112,7 @@ test-suite Tests main-is: Main.hs other-modules: Controllers.CourseControllerTests, + Controllers.GenerateControllerTests, Controllers.GraphControllerTests, Database.CourseQueriesTests, RequirementTests.ModifierTests,