diff --git a/.github/workflows/maintenance.yaml b/.github/workflows/maintenance.yaml index edea791..26023ef 100644 --- a/.github/workflows/maintenance.yaml +++ b/.github/workflows/maintenance.yaml @@ -10,6 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' - run: >- sudo apt update && sudo apt install wget && @@ -17,7 +21,7 @@ jobs: sudo bash ./llvm.sh 19 && (sudo rm /usr/bin/clang-format || exit 0) && sudo apt -y install clang-format-19 - - run: find src -type f | xargs -n1 clang-format-19 --dry-run -Werror + - run: gradle format build: timeout-minutes: 10 diff --git a/.gitignore b/.gitignore index 4790b95..0ed073a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ # Build artifacts .gradle build +src/testgen/dist-* diff --git a/build.gradle.kts b/build.gradle.kts index 2786744..4e3304a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -109,6 +109,13 @@ pitest { timestampedReports.set(false) } +// Test generation + +tasks.register("generateQuickCheckTests") { + workingDir = file("src/testgen") + commandLine = listOf("cabal", "run") +} + tasks.register("generateEvoSuiteTests") { classpath = evoSuiteDownload mainClass = "org.evosuite.EvoSuite" @@ -136,9 +143,21 @@ tasks.named("run") { standardInput = System.`in` } +tasks.register("repl") { + workingDir = file("src/testgen") + commandLine = listOf("cabal", "repl") + standardInput = System.`in` +} + +tasks.register("haddock") { + workingDir = file("src/testgen") + commandLine = listOf("cabal", "haddock", "--haddock-all") +} + tasks.register("format") { workingDir = file(rootDir) commandLine = listOf("sh", "-c", - "find src -type f | xargs -n1 sh -c 'clang-format -i $0; sed -i s/\\\\r//g $0'" + "(find src/ -type f -not -path 'src/testgen/*' | xargs -n1 sh -c 'clang-format -i $0; sed -i s/\\\\r//g $0') &&" + + "(cd src/testgen && ./format.sh)" ) } diff --git a/src/testgen/format.sh b/src/testgen/format.sh new file mode 100755 index 0000000..6ad56de --- /dev/null +++ b/src/testgen/format.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# This script checks for formatting errors in the Haskell code. + +# Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +# +# 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. + +grep_error_message() { + sed -r "s/^([^:]*):([^:]*):.*$/\1:\2: $1/g" +} + +cabal format || exit 1 + +# shellcheck disable=SC2266 +find . -type f -name "*.hs" | while IFS="" read -r file; do + grep -PHn '.{101,}$' "$file" | grep_error_message "Column exceeds 100 characters" + grep -PHn '\t$' "$file" | grep_error_message "Use of tabs" + grep -PHn '\s+$' "$file" | grep_error_message "Trailing whitespace" +done | sort -V | tee /dev/stderr | test "$(wc -l)" = 0 diff --git a/src/testgen/testgen.cabal b/src/testgen/testgen.cabal new file mode 100644 index 0000000..caa9267 --- /dev/null +++ b/src/testgen/testgen.cabal @@ -0,0 +1,26 @@ +cabal-version: 2.4 +name: testgen +version: 0.0.0.1 +license: Apache-2.0 +author: + Humberto Gomes + José Lopes + Mariana Rocha + +synopsis: Business layer facade test generator + +executable testgen + main-is: Main.hs + hs-source-dirs: testgen + other-modules: + FacadeTemplates + Generators + Java + TestClass + TestTemplate + + default-language: Haskell2010 + build-depends: + base >=4.15.1.0 && <5, + QuickCheck ^>=2.15.0.1, + process ^>=1.6.26.1 diff --git a/src/testgen/testgen/FacadeTemplates.hs b/src/testgen/testgen/FacadeTemplates.hs new file mode 100644 index 0000000..f34954e --- /dev/null +++ b/src/testgen/testgen/FacadeTemplates.hs @@ -0,0 +1,34 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +module FacadeTemplates ( + equalityTemplate + ) where + +import Java (assertEquals, toJavaExpression) +import TestTemplate (TestTemplate, genToTestTemplate) +import Test.QuickCheck (Gen, arbitrary) + +equalityTestGenerator :: Gen [String] +equalityTestGenerator = do + elem <- arbitrary :: Gen Int + return [assertEquals (toJavaExpression elem) (toJavaExpression elem)] + + -- Other usage examples + -- return [assertTrue (toJavaExpression elem ++ " == " ++ toJavaExpression elem)] + -- return [assertSame (toJavaExpression elem) (toJavaExpression elem)] + -- return $ assertThrows "Exception" [assertEquals "1" "1"] + +equalityTemplate :: TestTemplate +equalityTemplate = genToTestTemplate "equals" equalityTestGenerator 3 diff --git a/src/testgen/testgen/Generators.hs b/src/testgen/testgen/Generators.hs new file mode 100644 index 0000000..3918555 --- /dev/null +++ b/src/testgen/testgen/Generators.hs @@ -0,0 +1,363 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +module Generators (User(..)) where + +import Test.QuickCheck (Arbitrary(..), elements, choose, oneof, vectorOf, Gen, listOf, frequency) +import Java (JavaData(..), toJavaExpression) + +-- | MakeItFit user +data User = + Amateur + UUID -- ^ User's identifier + String -- ^ User's name + Int -- ^ User's age + Gender -- ^ User's gender + Float -- ^ User's weight + Int -- ^ User's height + Int -- ^ User's BPM + Int -- ^ User's level + String -- ^ User's address + String -- ^ User's phone + String -- ^ User's email + Float -- ^ User's index + [Activity] -- ^ User's activities + | + Occasional + UUID -- ^ User's identifier + String -- ^ User's name + Int -- ^ User's age + Gender -- ^ User's gender + Float -- ^ User's weight + Int -- ^ User's height + Int -- ^ User's BPM + Int -- ^ User's level + String -- ^ User's address + String -- ^ User's phone + String -- ^ User's email + Int -- ^ User's frequency + Float -- ^ User's index + [Activity] -- ^ User's activities + | + Professional + UUID -- ^ User's identifier + String -- ^ User's name + Int -- ^ User's age + Gender -- ^ User's gender + Float -- ^ User's weight + Int -- ^ User's height + Int -- ^ User's BPM + Int -- ^ User's level + String -- ^ User's address + String -- ^ User's phone + String -- ^ User's email + Int -- ^ User's frequency + String -- ^ User's specialization + Float -- ^ User's index + [Activity] -- ^ User's activities + deriving (Show, Eq) + +data Gender = Male | Female deriving (Show, Eq) + +instance JavaData Gender where + javaTypeName _ = "Gender" + toJavaExpression Male = "Gender.MALE" + toJavaExpression Female = "Gender.FEMALE" + +genUUID :: Gen UUID +genUUID = do + part1 <- vectorOf 8 (elements (['a'..'f'] ++ ['0'..'9'])) + part2 <- vectorOf 4 (elements (['a'..'f'] ++ ['0'..'9'])) + part3 <- vectorOf 4 (elements (['a'..'f'] ++ ['0'..'9'])) + part4 <- vectorOf 4 (elements (['a'..'f'] ++ ['0'..'9'])) + part5 <- vectorOf 12 (elements (['a'..'f'] ++ ['0'..'9'])) + return $ part1 ++ "-" ++ part2 ++ "-" ++ part3 ++ "-" ++ part4 ++ "-" ++ part5 + +genName :: Gen String +genName = elements + [ "Humberto" , "José" , "Mariana" , "André" , "Adriana" , "Bruno" , "Beatriz" , "Carlota" + , "Carlos" , "Dalila" , "Dinis" , "Eva", "Filipe" , "Hugo" , "Gabriel" , "Sofia" + , "Olívia" , "Henrique" , "Margarida" , "Elisabete" + ] + +genPhone :: Gen String +genPhone = do + secondDigit <- elements ['1', '2', '3', '6'] + rest <- sequence $ replicate 7 (elements ['0'..'9']) + return $ '9' : secondDigit : rest + +genEmail :: Gen String +genEmail = do + localPart <- listOf $ elements (['a'..'z'] ++ ['0'..'9']) + domain <- frequency + [ (32, return "gmail.com"), (30, return "yahoo.com"), (28, return "hotmail.com") + , (5, return "aol.com"), (2, return "hotmail.co.uk"), (2, return "hotmail.fr") + , (1, return "msn.com") + ] + return $ localPart ++ "@" ++ domain + +genAddress :: Gen String +genAddress = do + street <- genName + number <- elements [1..100] + city <- elements ["Braga", "Lisboa", "Porto", "Coimbra", "Leiria"] + return $ "Rua" ++ street ++ " " ++ show number ++ ", " ++ city + +genActivityName :: Gen String +genActivityName = elements + [ "Yoga", "Corrida", "Trail", "Musculação", "Caminhada", "Bicicleta", "Natação" + , "Pilates", "Boxe", "Zumba", "Body Pump", "Cycling" + ] + +genSpecialization :: Gen String +genSpecialization = elements + [ "Ballet", "Musculação", "Cardio", "Pilates", "Yoga", "Body Pump", "Localizada" + , "Zumba", "Natação", "Cycling", "Boxe" + ] + +instance Arbitrary User where + arbitrary = do + code <- genUUID + tag <- elements ["Amateur", "Occasional", "Professional"] + name <- genName + age <- elements [18..80] + gender <- elements [Male, Female] + weight <- elements [50.0..100.00] + height <- elements [150..195] + bpm <- elements [60..100] + level <- arbitrary + address <- genAddress + phone <- genPhone + email <- genEmail + index <- choose (0.0, 10.0) + n <- choose (1, 30) + activities <- vectorOf n arbitrary + case tag of + "Amateur" -> return $ Amateur code name age gender weight height bpm level address phone email index activities + "Occasional" -> do + freq <- elements [1..14] + return $ Occasional code name age gender weight height bpm level address phone email freq index activities + "Professional" -> do + freq <- elements [1..21] + spec <- genSpecialization + return $ Professional code name age gender weight height bpm level address phone email freq spec index activities + +instance JavaData User where + javaTypeName _ = "User" + toJavaExpression u = case u of + Amateur uuid name age gender weight height bpm lvl addr phone email idx acts -> + "new Amateur(" ++ intercalate ", " (map toJavaExpression + [uuid, name, age, gender, weight, height, bpm, lvl, addr, phone, email, idx]) ++ + ", " ++ toJavaExpression acts ++ ")" + + Occasional uuid name age gender weight height bpm lvl addr phone email freq idx acts -> + "new Occasional(" ++ intercalate ", " (map toJavaExpression + [uuid, name, age, gender, weight, height, bpm, lvl, addr, phone, email, freq, idx]) ++ + ", " ++ toJavaExpression acts ++ ")" + + Professional uuid name age gender weight height bpm lvl addr phone email freq spec idx acts -> + "new Professional(" ++ intercalate ", " (map toJavaExpression + [uuid, name, age, gender, weight, height, bpm, lvl, addr, phone, email, freq, spec, idx]) ++ + ", " ++ toJavaExpression acts ++ ")" + +-- | MakeItFit Date +data MakeItFitDate = MakeItFitDate (Int, Int, Int) deriving (Show, Eq, Ord) + +type UUID = String + +instance Arbitrary MakeItFitDate where + arbitrary = do + year <- elements [2001..2025] + month <- elements [1..12] + let daysInMonth m y + | m == 2 = if y `mod` 4 == 0 && (y `mod` 100 /= 0 || y `mod` 400 == 0) then 29 else 28 + | m `elem` [4, 6, 9, 11] = 30 + | otherwise = 31 + day <- elements [1..daysInMonth month year] + return $ MakeItFitDate (year, month, day) + +instance JavaData MakeItFitDate where + javaTypeName _ = "MakeItFitDate" + toJavaExpression (MakeItFitDate (y, m, d)) = + "new MakeItFitDate(" ++ show y ++ ", " ++ show m ++ ", " ++ show d ++ ")" + +-- | MakeItFit activity +data Activity + = PushUp + UUID -- ^ Activity's user code identifier + UUID -- ^ Activity's code + MakeItFitDate -- ^ Date when the activity was performed + Int -- ^ Expected duration of the activity (in minutes) + String -- ^ Designation for the activity + String -- ^ Name of the activity + String -- ^ Specialization of the activity + Int -- ^ Activity's duration + Int -- ^ Calories wasted during the activity + Int -- ^ Number of repetitions + Int -- ^ Number of series + | Running + UUID -- ^ Activity's user code identifier + UUID -- ^ Activity's code + MakeItFitDate -- ^ Date when the activity was performed + Int -- ^ Expected duration of the activity (in minutes) + String -- ^ Designation for the activity + String -- ^ Name of the activity + String -- ^ Specialization of the activity + Int -- ^ Activity's duration + Int -- ^ Calories wasted during the activity + Double -- ^ Distance covered + Double -- ^ Average speed + | Trail + UUID -- ^ Activity's user code identifier + UUID -- ^ Activity's code + MakeItFitDate -- ^ Date when the activity was performed + Int -- ^ Expected duration of the activity (in minutes) + String -- ^ Designation for the activity + String -- ^ Name of the activity + String -- ^ Specialization of the activity + Int -- ^ Activity's duration + Int -- ^ Calories wasted during the activity + Double -- ^ Distance covered + Double -- ^ Elevation gain + Double -- ^ Elevation loss + TrailType -- ^ Type of trail (Easy, Medium, Hard) + | WeightSquat + UUID -- ^ Activity's user code identifier + UUID -- ^ Activity's code + MakeItFitDate -- ^ Date when the activity was performed + Int -- ^ Expected duration of the activity (in minutes) + String -- ^ Designation for the activity + String -- ^ Name of the activity + String -- ^ Specialization of the activity + Int -- ^ Activity's duration + Int -- ^ Calories wasted during the activity + Int -- ^ Number of repetitions + Int -- ^ Number of series + Double -- ^ Weight used in the repetitions + deriving (Show, Eq) + +data TrailType = TrailEasy | TrailMedium | TrailHard + deriving (Show, Eq) + +genDesignation :: Gen String +genDesignation = elements + [ "Treino das 9h", "Treino do 12h", "Treino das 15h", "Treino das 17h", "Treino das 19h" + , "Treino para emagrecer", "Passeio"] + +instance JavaData TrailType where + javaTypeName _ = "TrailType" + toJavaExpression TrailEasy = "TrailType.EASY" + toJavaExpression TrailMedium = "TrailType.MEDIUM" + toJavaExpression TrailHard = "TrailType.HARD" + +instance Arbitrary Activity where + arbitrary = do + tag <- elements ["PushUp", "Running", "Trail", "WeightSquat"] + userCode <- genUUID + activityCode <- genUUID + date <- arbitrary + expDur <- elements [10,15,20,30,45,60] + designation <- genDesignation + name <- genActivityName + let spec = tag + dur <- elements [5,10,15,20,30,45,60] + cal <- elements [20,50,100,150,172,200,300] + case tag of + "PushUp" -> do + reps <- elements [5..100] + series <- elements [1..10] + return $ PushUp userCode activityCode date expDur designation name spec dur cal reps series + "Running" -> do + dist <- choose (1.0, 45.0) + speed <- choose (6.0, 15.0) + return $ Running userCode activityCode date expDur designation name spec dur cal dist speed + "Trail" -> do + dist <- frequency[(5, choose(2.0, 10.0)),(3, choose(10.0, 25.0)),(2, choose(25.0, 50.0))] + elevGain <- choose (0.0, 20.0) + elevLoss <- choose (0.0, 20.0) + trailtype <- arbitrary + return $ Trail userCode activityCode date expDur designation name spec dur cal dist elevGain elevLoss trailtype + "WeightSquat" -> do + reps <- elements [5..30] + series <- elements [1..8] + weight <- choose (10.0, 200.0) + return $ WeightSquat userCode activityCode date expDur designation name spec dur cal reps series weight + +instance JavaData Activity where + javaTypeName _ = "Activity" + toJavaExpression a = case a of + PushUp uc ac date ed des name spec dur cal reps series -> + "new PushUp(" ++ intercalate ", " (map toJavaExpression + [uc, ac]) ++ ", " ++ + toJavaExpression date ++ ", " ++ + intercalate ", " (map toJavaExpression + [ed, des, name, spec, dur, cal, reps, series]) ++ ")" + + Running uc ac date ed des name spec dur cal dist speed -> + "new Running(" ++ intercalate ", " (map toJavaExpression + [uc, ac]) ++ ", " ++ + toJavaExpression date ++ ", " ++ + intercalate ", " (map toJavaExpression + [ed, des, name, spec, dur, cal]) ++ ", " ++ + toJavaExpression dist ++ ", " ++ toJavaExpression speed ++ ")" + + Trail uc ac date ed des name spec dur cal dist eg el tt -> + "new Trail(" ++ intercalate ", " (map toJavaExpression + [uc, ac]) ++ ", " ++ + toJavaExpression date ++ ", " ++ + intercalate ", " (map toJavaExpression + [ed, des, name, spec, dur, cal]) ++ ", " ++ + toJavaExpression dist ++ ", " ++ + toJavaExpression eg ++ ", " ++ + toJavaExpression el ++ ", " ++ + toJavaExpression tt ++ ")" + + WeightSquat uc ac date ed des name spec dur cal reps series weight -> + "new WeightSquat(" ++ intercalate ", " (map toJavaExpression + [uc, ac]) ++ ", " ++ + toJavaExpression date ++ ", " ++ + intercalate ", " (map toJavaExpression + [ed, des, name, spec, dur, cal, reps, series, weight]) ++ ")" + +-- | MakeItFit training plan +data TrainingPlan = TrainingPlan + UUID -- ^ user code + UUID -- ^ training plan code + String -- ^ training plan name + [MyTuple Int Activity] -- ^ activities (list of (Integer, Activity) tuples) + MakeItFitDate -- ^ date when the training plan was created + deriving (Show, Eq) + +data MyTuple a b = MyTuple a b + deriving (Eq, Show) + +instance Arbitrary TrainingPlan where + arbitrary = do + userCode <- genUUID + planCode <- genUUID + letter <- elements ['A'..'Z'] + let name = "Plano " ++ [letter] + n <- choose (1, 7) + activities <- vectorOf n arbitrary + date <- arbitrary + return $ TrainingPlan userCode planCode name (zip [1..n] activities) date + +instance JavaData TrainingPlan where + toJava (TrainingPlan name duration activities) = + "new TrainingPlan(" ++ + toJava name ++ ", " ++ + show duration ++ ", " ++ + "List.of(" ++ intercalate ", " (map toJava activities) ++ ")" ++ + ")" + diff --git a/src/testgen/testgen/Java.hs b/src/testgen/testgen/Java.hs new file mode 100644 index 0000000..9fbd4a9 --- /dev/null +++ b/src/testgen/testgen/Java.hs @@ -0,0 +1,183 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +{-# LANGUAGE FlexibleInstances #-} + +-- | Java code generation utilities +module Java + ( + JavaData(..) + , indent + , decorateTest + , assertTrue + , assertEquals + , assertSame + , assertThrows) where + +import Data.Char (isAscii, ord) +import Data.List (intercalate) +import Numeric (showHex) + +-- JavaData definition +class JavaData a where + -- | Gets the equivalent Java name for this Haskell type + javaTypeName :: a -- ^ Input value + -> String -- ^ Java type name + + -- | + -- Creates a Java expression that evaluates to the given value. + -- + -- Example output: @Arrays.asList(1, 2, 3)@ + toJavaExpression :: a -- ^ Input value + -> String -- ^ Java expression + + -- | + -- Creates a Java variable declaration, where the variable is initialized to the given value + -- + -- Example output: @List list = Arrays.asList(1, 2, 3)@ + toJavaVariable :: String -- ^ Variable name + -> a -- ^ Input value + -> String -- ^ Java variable declation + toJavaVariable name obj = javaTypeName obj ++ " " ++ name ++ " = " ++ toJavaExpression obj ++ ";" + +-- Basic scalar types + +instance JavaData Bool where + javaTypeName = const "bool" + toJavaExpression False = "false" + toJavaExpression True = "true" + +instance JavaData Int where + javaTypeName = const "int" + toJavaExpression = show + +instance JavaData Integer where + javaTypeName = const "int" + toJavaExpression = show + +instance JavaData Float where + javaTypeName = const "float" + toJavaExpression = show + +instance JavaData Double where + javaTypeName = const "double" + toJavaExpression = show + +pad :: Int -> a -> [a] -> [a] +pad n f xs = replicate (n - length xs) f ++ xs + +toJavaCharacter '"' = "\\\"" +toJavaCharacter '\'' = "\\'" +toJavaCharacter '\\' = "\\\\" +toJavaCharacter '\t' = "\\t" +toJavaCharacter '\b' = "\\b" +toJavaCharacter '\n' = "\\n" +toJavaCharacter '\r' = "\\r" +toJavaCharacter '\f' = "\\f" +toJavaCharacter c + | isAscii c = [c] + | otherwise = "\\u" ++ pad 4 '0' (showHex (ord c) "") + +instance JavaData Char where + javaTypeName = const "char" + toJavaExpression c = "\'" ++ toJavaCharacter c ++ "\'" + +instance JavaData String where + javaTypeName = const "String" + toJavaExpression s = "\"" ++ concat (map toJavaCharacter s) ++ "\"" + +-- Lists + +toJavaExpressionList :: JavaData a => [a] -> String +toJavaExpressionList xs = "Arrays.asList(" ++ intercalate ", " (map toJavaExpression xs) ++ ")" + +instance JavaData [Bool] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +instance JavaData [Int] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +instance JavaData [Integer] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +instance JavaData [Float] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +instance JavaData [Double] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +instance JavaData [String] where + javaTypeName = const "List" + toJavaExpression = toJavaExpressionList + +-- Utilities + +-- | Indents a sequence of lines with 4 spaces +indent :: [String] -> [String] +indent = map (" " ++) + +-- | +-- Puts the body of a test in a method declaration +-- +-- Example output: +-- +-- @ +-- \@Test +-- void testName() { +-- testContents +-- } +-- @ +decorateTest :: String -- ^ Name of the test method + -> [String] -- ^ Lines constituting the body of the test method + -> [String] -- ^ Lines of the test method +decorateTest name test = concat [["@Test", "void " ++ name ++ "() {"], indent test, ["}"]] + +-- Assertions + +-- | @assertTrue@ statement generator +assertTrue :: String -- ^ A Java boolean expression + -> String -- ^ A Java assertion statement +assertTrue c = "assertTrue(" ++ c ++ ");" + +-- | @assertEquals@ statement generator +assertEquals :: String -- ^ A Java expression for the expected value + -> String -- ^ A Java expression for the obtained value + -> String -- ^ A Java assertion statement +assertEquals e g = "assertEquals(" ++ e ++ ", " ++ g ++ ");" + +-- | @assertSame@ statement generator +assertSame :: String -- ^ A Java expression for the expected value + -> String -- ^ A Java expression for the obtained value + -> String -- ^ A Java assertion statement +assertSame e g = "assertSame(" ++ e ++ ", " ++ g ++ ");" + +-- | @assertThrows@ statement generator +assertThrows :: String -- ^ Name of the expected exception class + -> [String] -- ^ Lines where the exception should occur + -> [String] -- ^ A Java assertion statement +assertThrows e b = + [ + "assertThrows(" + , (" " ++ e ++ ".class,") + , " () -> {" + ] ++ (indent (indent b)) ++ + [ + " }" + , ");" + ] diff --git a/src/testgen/testgen/Main.hs b/src/testgen/testgen/Main.hs new file mode 100644 index 0000000..cfd672e --- /dev/null +++ b/src/testgen/testgen/Main.hs @@ -0,0 +1,18 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +module Main where + +main :: IO () +main = putStrLn "Hello, Haskell!" diff --git a/src/testgen/testgen/TestClass.hs b/src/testgen/testgen/TestClass.hs new file mode 100644 index 0000000..97d0668 --- /dev/null +++ b/src/testgen/testgen/TestClass.hs @@ -0,0 +1,77 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +module TestClass (generateTestClass) where + +import Data.List (intercalate) +import Java (indent) +import TestTemplate (generateTestsFromTemplate) + +import System.Process (StdStream(CreatePipe), createProcess, proc, std_in, std_out, waitForProcess) +import System.IO (hClose, hGetContents, hPutStr) +import Control.Exception (bracket, evaluate) + +import FacadeTemplates + +templates = + [ + equalityTemplate -- TODO: remove this simple template only to show the desired architecture + ] + +generateTests :: IO [String] +generateTests = do + tests <- sequence $ map generateTestsFromTemplate templates + return $ intercalate [""] tests + +generateUnformattedTestClass :: IO [String] +generateUnformattedTestClass = do + tests <- generateTests + return + $ concat + [ [ "import java.util.Arrays;" + , "import java.util.List;" + , "" + , "import MakeItFit.users.User;" + , "" + , "public class MakeItFitTest {" + ] + , indent tests + , ["}"] + ] + +format :: String -> IO String +format source = do + (Just stdin, Just stdout, _, process) <- createProcess + (proc "clang-format" ["--assume-filename=MakeItFitTest.java"]) + { std_in = CreatePipe, std_out = CreatePipe } + + bracket + (return ()) + (\_ -> hClose stdin >> hClose stdout) + (\_ -> do + hPutStr stdin source + hClose stdin + + output <- hGetContents stdout + evaluate (length output) -- Force deep strict evaluation + + waitForProcess process + return output + ) + +-- | Generates the test class for the applications facade +generateTestClass :: IO String +generateTestClass = do + lines <- generateUnformattedTestClass + format $ unlines lines diff --git a/src/testgen/testgen/TestTemplate.hs b/src/testgen/testgen/TestTemplate.hs new file mode 100644 index 0000000..0afe3f9 --- /dev/null +++ b/src/testgen/testgen/TestTemplate.hs @@ -0,0 +1,54 @@ +-- Copyright 2025 Humberto Gomes, José Lopes, Mariana Rocha +-- +-- 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. + +-- | Test template definition +module TestTemplate (TestTemplate(..), genToTestTemplate, generateTestsFromTemplate) where + +import Data.List (intercalate) +import Java (decorateTest) +import Test.QuickCheck.Gen (Gen, generate) + +-- Test template data structure and helpers + +-- | A template used for test generation +data TestTemplate = + TestTemplate + String -- ^ Test name + (IO [String]) -- ^ Generator function of the lines in the test's body + Int -- ^ Number of tests of this type to generate + +-- | Creates a 'TestTemplate' from a QuickCheck generator +genToTestTemplate :: String -- ^ Test name + -> Gen [String] -- ^ QuickCheck generator of the lines of the test's body + -> Int -- ^ Number of tests + -> TestTemplate -- ^ Output test template +genToTestTemplate name gen n = TestTemplate name (generate gen) n + +-- Test generation + +getTestNumber :: Int -> Int -> String +getTestNumber 1 _ = "" +getTestNumber _ i = show i + +generateTestFromTemplate :: TestTemplate -> Int -> IO [String] +generateTestFromTemplate (TestTemplate name sampler n) i = do + body <- sampler + return $ decorateTest (name ++ getTestNumber n i) body + +-- | Generates all tests from a template +generateTestsFromTemplate :: TestTemplate -- ^ Test template to use for generation + -> IO [String] -- ^ Lines of the generated tests +generateTestsFromTemplate t@(TestTemplate _ _ n) = do + tests <- sequence $ zipWith generateTestFromTemplate (repeat t) [1 .. n] + return $ intercalate [""] tests