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..5aba521 --- /dev/null +++ b/src/testgen/testgen/Generators.hs @@ -0,0 +1,32 @@ +-- 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, listOf) + +-- | MakeItFit user +data User = + Beginner + String -- ^ User's name + | + Intermediate + String -- ^ User's name + deriving (Show, Eq) + +instance Arbitrary User where + arbitrary = do + constructor <- elements [Beginner, Intermediate] + name <- listOf $ elements (['A'..'Z'] ++ ['a'..'z'] ++ [' ']) + return $ constructor name 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