diff --git a/.gitignore b/.gitignore index 1a5c77a..97b89e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/ +build/ .vscode *.o *.tab.hpp diff --git a/Makefile b/Makefile index 1a6757c..30bcb2f 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,14 @@ # Based on https://stackoverflow.com/a/52036564 which is well worth reading! -CXXFLAGS += -std=c++20 -W -Wall -g -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -fsanitize=address -static-libasan -O0 -rdynamic -I include +CXXFLAGS += -std=c++20 -W -Wall -g -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -fsanitize=address -static-libasan -O0 -rdynamic --coverage -I include SOURCES := $(wildcard src/*.cpp) -DEPENDENCIES := $(patsubst %.cpp,%.d,$(SOURCES)) +DEPENDENCIES := $(patsubst src/%.cpp,build/%.d,$(SOURCES)) -OBJECTS := $(patsubst %.cpp,%.o,$(SOURCES)) -OBJECTS += src/parser.tab.o src/lexer.yy.o +OBJECTS := $(patsubst src/%.cpp,build/%.o,$(SOURCES)) +OBJECTS += build/parser.tab.o build/lexer.yy.o - -.PHONY: default clean with_coverage coverage +.PHONY: default clean coverage default: bin/c_compiler @@ -19,35 +18,26 @@ bin/c_compiler: $(OBJECTS) -include $(DEPENDENCIES) -%.o: %.cpp Makefile +build/%.o: src/%.cpp Makefile + @mkdir -p $(@D) g++ $(CXXFLAGS) -MMD -MP -c $< -o $@ -src/parser.tab.cpp src/parser.tab.hpp: src/parser.y - bison -v -d src/parser.y -o src/parser.tab.cpp - -src/lexer.yy.cpp : src/lexer.flex src/parser.tab.hpp - flex -o src/lexer.yy.cpp src/lexer.flex +build/parser.tab.cpp build/parser.tab.hpp: src/parser.y + @mkdir -p build + bison -v -d src/parser.y -o build/parser.tab.cpp -with_coverage : CXXFLAGS += --coverage -with_coverage : bin/c_compiler +build/lexer.yy.cpp: src/lexer.flex build/parser.tab.hpp + @mkdir -p build + flex -o build/lexer.yy.cpp src/lexer.flex -coverage : coverage/index.html - -coverage/index.html : +coverage: + @rm -rf coverage/ @mkdir -p coverage -# somehow lexer and parser coverage info are available but not accurate. To exclude them use: -# lcov -c --no-external --exclude "`pwd`/src/lexer.*" --exclude "`pwd`/src/parser.*" -d . -o coverage/cov.info - lcov -c --no-external -d . -o coverage/cov.info + lcov -c --no-external --exclude "`pwd`/src/lexer.*" --exclude "`pwd`/src/parser.*" --exclude "`pwd`/build/*" -d . -o coverage/cov.info genhtml coverage/cov.info -o coverage @find . -name "*.gcda" -delete clean : @rm -rf coverage/ - @rm -rf src/*.o - @rm -rf src/*.d - @rm -rf src/*.gcno + @rm -rf build/ @rm -rf bin/ - @rm -f src/*.tab.hpp - @rm -f src/*.tab.cpp - @rm -f src/*.yy.cpp - @rm -f src/*.output diff --git a/docs/c_compiler.md b/docs/c_compiler.md index 683515c..350e748 100644 --- a/docs/c_compiler.md +++ b/docs/c_compiler.md @@ -111,6 +111,7 @@ Here is a list of intermediate features that you might like to implement once th * the `enum` keyword * `switch` statements * the `break` and `continue` keywords +* ternary operator (`x ? y : z`) Here is a list of more advanced features like you might like to implement once the basic and intermediate features are working. diff --git a/docs/coverage.md b/docs/coverage.md index 651633f..af4377d 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -1,7 +1,7 @@ Coverage information ==================== -If you want to know which part of your code is executed when running your compiler on a file you can build your compiler with `make with_coverage`, run your compiler on the file, then run `make coverage`. +If you want to know which part of your code is executed when running your compiler on a file you can run your compiler on the file, then run `make coverage`. This will generate a webpage `coverage/index.html` with a listing of all the source files and for each source file a listing of the number of times each line has been executed. diff --git a/scripts/test.py b/scripts/test.py index 97fc9c3..f3c70a5 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -7,7 +7,7 @@ This script will also generate a JUnit XML file, which can be used to integrate with CI/CD pipelines. -Usage: test.py [-h] [-m] [-s] [--version] [--no_clean | --coverage] [dir] +Usage: test.py [-h] [-m] [-s] [--version] [--no_clean] [--coverage] [dir] Example usage: scripts/test.py compiler_tests/_example @@ -32,6 +32,7 @@ from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed from typing import List, Optional +from http.server import HTTPServer, SimpleHTTPRequestHandler RED = "\033[31m" @@ -321,29 +322,56 @@ def clean() -> bool: return False return True -def make(with_coverage: bool, silent: bool) -> bool: +def make(silent: bool) -> bool: """ Wrapper for make bin/c_compiler. Return True if successful, False otherwise """ print(GREEN + "Running make..." + RESET) + return_code, error_msg, _ = run_subprocess( + cmd=["make", "-C", PROJECT_LOCATION, "bin/c_compiler"], timeout=BUILD_TIMEOUT_SECONDS, silent=silent + ) + if return_code != 0: + print(RED + "Error when making:", error_msg + RESET) + return False - cmd = ["make", "-C", PROJECT_LOCATION, "bin/c_compiler"] - if with_coverage: - # Run coverage if needed - print(GREEN + "Making with coverage..." + RESET) - shutil.rmtree(COVERAGE_FOLDER, ignore_errors=True) - cmd = ["make", "-C", PROJECT_LOCATION, "with_coverage"] + return True - return_code, error_msg, _ = run_subprocess(cmd=cmd, timeout=BUILD_TIMEOUT_SECONDS, silent=silent) +def coverage() -> bool: + """ + Wrapper for make coverage. + Return True if successful, False otherwise + """ + print(GREEN + "Running make coverage..." + RESET) + return_code, error_msg, _ = run_subprocess( + cmd=["make", "-C", PROJECT_LOCATION, "coverage"], timeout=BUILD_TIMEOUT_SECONDS, silent=True + ) if return_code != 0: - print(RED + "Error when making:", error_msg + RESET) + print(RED + "Error when making coverage:", error_msg + RESET) return False - return True +def serve_coverage_forever(host: str, port: int): + """ + Starts a HTTP server which serves the coverage folder forever until Ctrl+C + is pressed. + """ + class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, directory=None, **kwargs): + super().__init__(*args, directory=COVERAGE_FOLDER, **kwargs) + + def log_message(self, format, *args): + pass + + httpd = HTTPServer((host, port), Handler) + print(GREEN + "Serving coverage on" + RESET + f" http://{host}:{port}/ ... (Ctrl+C to exit)") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print(RED + "\nServer has been stopped!" + RESET) + def process_result( result: Result, xml_file: JUnitXMLFile, @@ -402,7 +430,7 @@ def run_tests(args, xml_file: JUnitXMLFile): print("\n>> Test Summary: " + GREEN + f"{passing} Passed, " + RED + f"{total-passing} Failed" + RESET) def parse_args(): - """" + """ Wrapper for argument parsing. """ parser = argparse.ArgumentParser() @@ -433,16 +461,14 @@ def parse_args(): action="version", version=f"BetterTesting {__version__}" ) - # Coverage cannot be perfomed without rebuilding the compiler - group = parser.add_mutually_exclusive_group(required=False) - group.add_argument( - "--no_clean", + parser.add_argument( + '--no_clean', action="store_true", default=False, - help="Do no clean the repository before testing. This will make it " + help="Don't clean the repository before testing. This will make it " "faster but it can be safer to clean if you have any compilation issues." ) - group.add_argument( + parser.add_argument( "--coverage", action="store_true", default=False, @@ -461,12 +487,17 @@ def main(): # Clean the repo if required and exit if this fails. exit(2) - if not make(with_coverage=args.coverage, silent=args.short): + if not make(silent=args.short): exit(3) with JUnitXMLFile(J_UNIT_OUTPUT_FILE) as xml_file: run_tests(args, xml_file) + if args.coverage: + if not coverage(): + exit(4) + serve_coverage_forever('0.0.0.0', 8000) + if __name__ == "__main__": try: main() diff --git a/scripts/test.sh b/scripts/test.sh index 151f81b..95015c8 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -10,16 +10,9 @@ if [ "${DONT_CLEAN:-}" != "1" ]; then make clean fi -if [ "${COVERAGE:-}" == "1" ]; then - rm -rf coverage - set -e - make with_coverage - set +e -else - set -e - make bin/c_compiler - set +e -fi +set -e +make bin/c_compiler +set +e mkdir -p bin mkdir -p bin/output diff --git a/src/parser.y b/src/parser.y index 4243310..0100d04 100644 --- a/src/parser.y +++ b/src/parser.y @@ -11,6 +11,7 @@ extern FILE *yyin; int yylex(void); void yyerror(const char *); + int yylex_destroy(void); } // Represents the value associated with any kind of AST node. @@ -202,5 +203,7 @@ Node *ParseAST(std::string file_name) } g_root = nullptr; yyparse(); + fclose(yyin); + yylex_destroy(); return g_root; } diff --git a/src/parser_full.y.example b/src/parser_full.y.example index 29a4148..20c1cf0 100644 --- a/src/parser_full.y.example +++ b/src/parser_full.y.example @@ -7,6 +7,7 @@ extern FILE *yyin; int yylex(void); void yyerror(const char *); + int yylex_destroy(void); } // Represents the value associated with any kind of AST node. @@ -468,5 +469,7 @@ Node *ParseAST(std::string file_name) } g_root = nullptr; yyparse(); + fclose(yyin); + yylex_destroy(); return g_root; }