From ff3b511384e957632278c1acd23acb78b3174b09 Mon Sep 17 00:00:00 2001 From: Gabriel Ferreira Date: Sat, 8 Jan 2022 23:09:57 -0300 Subject: [PATCH] build: Bugfixes and refactoring of ns3 and CMake Including: - add missing command for introspected doxygen - run get_version.sh before running doxygen - use find_package(Doxygen) to get the doxygen executable - silence python and sqlite find_package warnings - return cmake returncode if configuration fails - require GTK3 3.22 - link all libraries to print-introspected-doxygen - replace shell with use_shell for variable name - revert wrong changes to propagation of return codes and add test - disable pch when ccache is found - make --enable-sudo a post-build step and a runtime option - add docs subparser - add enable-sudo option - refactor positional argument values - fix --check option and add shell option - replace --no-task-lines with --quiet - replace --nowaf with --no-build - replace --run --run-no-build with run (--no-build) - replace ns3 documentation related arguments with targets - document test-ns3.py - export include directories used by ns3 libraries - refactor CMake documentation dependency checking and behavior - add --allow-run-as-root for running MPI examples on the CI --- CMakeLists.txt | 13 +- .../custom_modules/ns3_coverage.cmake | 4 +- .../custom_modules/ns3_module_macros.cmake | 9 + .../waf_workaround_c4cache.cmake | 1 + buildsupport/macros_and_definitions.cmake | 156 ++-- ns3 | 392 ++++++---- src/config-store/CMakeLists.txt | 3 + src/core/model/command-line.cc | 2 +- src/core/model/example-as-test.cc | 4 +- src/fd-net-device/CMakeLists.txt | 5 +- src/stats/CMakeLists.txt | 2 +- utils/CMakeLists.txt | 6 +- utils/tests/test-ns3.py | 730 ++++++++++++++---- 13 files changed, 970 insertions(+), 357 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 52cc51e23..a3aefa3cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,14 +8,12 @@ mark_as_advanced(CCACHE_FOUND) find_program(CCACHE_FOUND ccache) if(CCACHE_FOUND) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) - set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) - message(STATUS "CCache is enabled") - - # Configure ccache for pch headers - execute_process( - COMMAND ${CCACHE_FOUND} - --set-config=sloppiness=pch_defines,time_macros,include_file_mtime + message( + STATUS "CCache is enabled. Precompiled headers are disabled by default." ) + set(NS3_PRECOMPILE_HEADERS OFF CACHE BOOL "") +else() + set(NS3_PRECOMPILE_HEADERS ON CACHE BOOL "") endif() # ############################################################################## @@ -53,7 +51,6 @@ option(NS3_COVERAGE "Enable code coverage measurements and report generation" option(NS3_COVERAGE_ZERO_COUNTERS "Zero lcov counters before running. Requires NS3_COVERAGE=ON" OFF ) -option(NS3_DOCS "Generate documentation" OFF) option(NS3_INCLUDE_WHAT_YOU_USE "Use IWYU to determine unnecessary headers included" OFF ) diff --git a/buildsupport/custom_modules/ns3_coverage.cmake b/buildsupport/custom_modules/ns3_coverage.cmake index 55b19094b..9af092d07 100644 --- a/buildsupport/custom_modules/ns3_coverage.cmake +++ b/buildsupport/custom_modules/ns3_coverage.cmake @@ -31,8 +31,8 @@ if(${NS3_COVERAGE}) if(${NS3_COVERAGE_ZERO_COUNTERS}) set(zero_counters "--lcov-zerocounters") endif() - # The following target will run test.py --nowaf to generate the code coverage - # files .gcno and .gcda output will be in ${CMAKE_BINARY_DIR} a.k.a. + # The following target will run test.py --no-build to generate the code + # coverage files .gcno and .gcda output will be in ${CMAKE_BINARY_DIR} a.k.a. # cmake_cache or cmake-build-${build_suffix} # Create output directory for coverage info and html diff --git a/buildsupport/custom_modules/ns3_module_macros.cmake b/buildsupport/custom_modules/ns3_module_macros.cmake index bb93366a0..8fc2ab642 100644 --- a/buildsupport/custom_modules/ns3_module_macros.cmake +++ b/buildsupport/custom_modules/ns3_module_macros.cmake @@ -132,6 +132,15 @@ macro( ns${NS3_VER}-${libname}${build_profile_suffix} ) + # export include directories used by this library so that it can be used by + # 3rd-party consumers of ns-3 using find_package(ns3) this will automatically + # add the build/include path to them, so that they can ns-3 headers with + # + target_include_directories( + ${lib${libname}} PUBLIC $ + $ + ) + set(ns3-external-libs "${non_ns_libraries_to_link};${ns3-external-libs}" CACHE INTERNAL "list of non-ns libraries to link to NS3_STATIC and NS3_MONOLIB" diff --git a/buildsupport/custom_modules/waf_workaround_c4cache.cmake b/buildsupport/custom_modules/waf_workaround_c4cache.cmake index 606329e74..591fc4797 100644 --- a/buildsupport/custom_modules/waf_workaround_c4cache.cmake +++ b/buildsupport/custom_modules/waf_workaround_c4cache.cmake @@ -63,6 +63,7 @@ function(generate_c4che_cachepy) cache_cmake_flag(NS3_OPENFLOW "ENABLE_OPENFLOW" cache_contents) cache_cmake_flag(NS3_CLICK "NSCLICK" cache_contents) cache_cmake_flag(NS3_BRITE "ENABLE_BRITE" cache_contents) + cache_cmake_flag(NS3_ENABLE_SUDO "ENABLE_SUDO" cache_contents) cache_cmake_flag(NS3_PYTHON_BINDINGS "ENABLE_PYTHON_BINDINGS" cache_contents) string(APPEND cache_contents "EXAMPLE_DIRECTORIES = [") diff --git a/buildsupport/macros_and_definitions.cmake b/buildsupport/macros_and_definitions.cmake index 3755d469b..9ddd94fcf 100644 --- a/buildsupport/macros_and_definitions.cmake +++ b/buildsupport/macros_and_definitions.cmake @@ -24,6 +24,13 @@ set(NS3_INT64X64 "CAIRO" CACHE STRING "Int64x64 implementation") set(NS3_INT64X64 "DOUBLE" CACHE STRING "Int64x64 implementation") set_property(CACHE NS3_INT64X64 PROPERTY STRINGS INT128 CAIRO DOUBLE) +# Purposefully hidden option since we can't really do that safely from the CMake +# side +mark_as_advanced(NS3_ENABLE_SUDO) +option(NS3_ENABLE_SUDO + "Set executables ownership to root and enable the SUID flag" OFF +) + # WSLv1 doesn't support tap features if(EXISTS "/proc/version") file(READ "/proc/version" CMAKE_LINUX_DISTRO) @@ -232,6 +239,33 @@ macro(clear_global_cached_variables) ) endmacro() +# function used to search for package and program dependencies than store list +# of missing dependencies in the list whose name is stored in missing_deps +function(check_deps package_deps program_deps missing_deps) + set(local_missing_deps) + # Search for package dependencies + foreach(package ${package_deps}) + find_package(${package}) + if(NOT ${${package}_FOUND}) + list(APPEND local_missing_deps ${package}) + endif() + endforeach() + + # And for program dependencies + foreach(program ${program_deps}) + # CMake likes to cache find_* to speed things up, so we can't reuse names + # here or it won't check other dependencies + string(TOUPPER ${program} upper_${program}) + find_program(${upper_${program}} ${program}) + if(NOT ${upper_${program}}) + list(APPEND local_missing_deps ${program}) + endif() + endforeach() + + # Store list of missing dependencies in the parent scope + set(${missing_deps} ${local_missing_deps} PARENT_SCOPE) +endfunction() + # process all options passed in main cmakeLists macro(process_options) clear_global_cached_variables() @@ -573,8 +607,14 @@ macro(process_options) if(NOT ${GTK3_FOUND}) message(STATUS "GTK3 was not found. Continuing without it.") else() - message(STATUS "GTK3 was found.") + if(${GTK3_VERSION} VERSION_LESS 3.22) + set(GTK3_FOUND FALSE) + message(STATUS "GTK3 found with incompatible version ${GTK3_VERSION}") + else() + message(STATUS "GTK3 was found.") + endif() endif() + endif() endif() @@ -631,7 +671,7 @@ macro(process_options) endif() endif() - find_package(Python3 COMPONENTS Interpreter Development) + find_package(Python3 COMPONENTS Interpreter Development QUIET) set(ENABLE_PYTHON_BINDINGS OFF) if(${NS3_PYTHON_BINDINGS}) if(NOT ${Python3_FOUND}) @@ -667,11 +707,11 @@ macro(process_options) if(${ENABLE_TESTS}) add_custom_target(all-test-targets) - # Create a custom target to run test.py --nowaf Target is also used to + # Create a custom target to run test.py --no-build Target is also used to # produce code coverage output add_custom_target( run_test_py - COMMAND ${Python3_EXECUTABLE} test.py --nowaf + COMMAND ${Python3_EXECUTABLE} test.py --no-build WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} DEPENDS all-test-targets ) @@ -740,31 +780,35 @@ macro(process_options) endif() endif() - if(${NS3_DOCS}) - mark_as_advanced(DOXYGEN DOT DIA) - find_program(DOXYGEN doxygen REQUIRED) - find_program(DOT dot REQUIRED) - find_program(DIA dia REQUIRED) - if((NOT DOT) OR (NOT DIA)) - message( - FATAL_ERROR - "Dot and Dia are required by Doxygen docs." - "They're shipped within the graphviz and dia packages on Ubuntu" - ) - endif() + # checking for documentation dependencies and creating targets + + # First we check for doxygen dependencies + mark_as_advanced(DOXYGEN) + check_deps("" "doxygen;dot;dia" doxygen_docs_missing_deps) + if(doxygen_docs_missing_deps) + message( + STATUS + "docs: doxygen documentation not enabled due to missing dependencies: ${doxygen_docs_missing_deps}" + ) + else() + # We checked this already exists, but we need the path to the executable + find_package(Doxygen QUIET) + # Get introspected doxygen add_custom_target( run-print-introspected-doxygen COMMAND ${CMAKE_OUTPUT_DIRECTORY}/utils/ns${NS3_VER}-print-introspected-doxygen${build_profile_suffix} - print-introspected-doxygen > - ${PROJECT_SOURCE_DIR}/doc/introspected-doxygen.h + > ${PROJECT_SOURCE_DIR}/doc/introspected-doxygen.h + COMMAND + ${CMAKE_OUTPUT_DIRECTORY}/utils/ns${NS3_VER}-print-introspected-doxygen${build_profile_suffix} + --output-text > ${PROJECT_SOURCE_DIR}/doc/ns3-object.txt DEPENDS print-introspected-doxygen ) add_custom_target( run-introspected-command-line COMMAND ${CMAKE_COMMAND} -E env NS_COMMANDLINE_INTROSPECTION=.. - ${Python3_EXECUTABLE} ./test.py --nowaf --constrain=example + ${Python3_EXECUTABLE} ./test.py --no-build --constrain=example WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} DEPENDS all-test-targets # all-test-targets only exists if ENABLE_TESTS is # set to ON @@ -773,11 +817,11 @@ macro(process_options) file( WRITE ${PROJECT_SOURCE_DIR}/doc/introspected-command-line.h "/* This file is automatically generated by -CommandLine::PrintDoxygenUsage() from the CommandLine configuration -in various example programs. Do not edit this file! Edit the -CommandLine configuration in those files instead. -*/ -\n" + CommandLine::PrintDoxygenUsage() from the CommandLine configuration + in various example programs. Do not edit this file! Edit the + CommandLine configuration in those files instead. + */ + \n" ) add_custom_target( assemble-introspected-command-line @@ -790,48 +834,45 @@ CommandLine configuration in those files instead. ) add_custom_target( - doxygen - COMMAND ${DOXYGEN} ${PROJECT_SOURCE_DIR}/doc/doxygen.conf + update_doxygen_version + COMMAND ${PROJECT_SOURCE_DIR}/doc/ns3_html_theme/get_version.sh WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - DEPENDS run-print-introspected-doxygen assemble-introspected-command-line ) add_custom_target( - doxygen-no-build COMMAND ${DOXYGEN} ${PROJECT_SOURCE_DIR}/doc/doxygen.conf + doxygen + COMMAND ${DOXYGEN_EXECUTABLE} ${PROJECT_SOURCE_DIR}/doc/doxygen.conf WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS update_doxygen_version run-print-introspected-doxygen + assemble-introspected-command-line ) - mark_as_advanced( - SPHINX_EXECUTABLE - SPHINX_OUTPUT_HTML - SPHINX_OUTPUT_MAN - SPHINX_WARNINGS_AS_ERRORS - EPSTOPDF - PDFLATEX - LATEXMK - CONVERT - DVIPNG + add_custom_target( + doxygen-no-build + COMMAND ${DOXYGEN_EXECUTABLE} ${PROJECT_SOURCE_DIR}/doc/doxygen.conf + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS update_doxygen_version ) - find_package(Sphinx REQUIRED) - find_program(EPSTOPDF epstopdf) - find_program(PDFLATEX pdflatex) - find_program(LATEXMK latexmk) - find_program(CONVERT convert) - find_program(DVIPNG dvipng) - if((NOT EPSTOPDF) - OR (NOT PDFLATEX) - OR (NOT LATEXMK) - OR (NOT CONVERT) - OR (NOT DVIPNG) + endif() + + # Now we check for sphinx dependencies + mark_as_advanced( + SPHINX_EXECUTABLE SPHINX_OUTPUT_HTML SPHINX_OUTPUT_MAN + SPHINX_WARNINGS_AS_ERRORS + ) + + # Check deps accepts a list of packages, list of programs and name of the + # return variable + check_deps( + "Sphinx" "epstopdf;pdflatex;latexmk;convert;dvipng" + sphinx_docs_missing_deps + ) + if(sphinx_docs_missing_deps) + message( + STATUS + "docs: sphinx documentation not enabled due to missing dependencies: ${sphinx_docs_missing_deps}" ) - message( - FATAL_ERROR - "Convert, Dvipng, Epstopdf, Pdflatex, Latexmk and fncychap.sty are required by Sphinx docs." - " They're shipped within the dvipng, imagemagick, texlive, texlive-font-utils and latexmk packages on Ubuntu." - " Imagemagick may fail due to a security policy issue." - " https://gitlab.com/nsnam/ns-3-dev/-/blob/master/utils/tests/gitlab-ci-doc.yml#L10-11" - ) - endif() + else() add_custom_target(sphinx COMMENT "Building sphinx documents") function(sphinx_target targetname) @@ -846,6 +887,7 @@ CommandLine configuration in those files instead. sphinx_target(models) sphinx_target(tutorial) endif() + # end of checking for documentation dependencies and creating targets # Process core-config if(${NS3_INT64X64} MATCHES "INT128") diff --git a/ns3 b/ns3 index 9ef5bcb61..931d29b41 100755 --- a/ns3 +++ b/ns3 @@ -59,6 +59,16 @@ def parse_args(argv): parser = argparse.ArgumentParser(description="ns-3 wrapper for the CMake build system") sub_parser = parser.add_subparsers() + parser_build = sub_parser.add_parser('build', + help=('Accepts a list of targets to build,' + ' or builds the entire project if no target is given')) + parser_build.add_argument('build', + help='Build the entire project or the specified target and dependencies', + action="store", nargs='*', default=None) + parser_build.add_argument('--dry-run', + help="Do not execute the commands", + action="store_true", default=None, dest="build_dry_run") + parser_configure = sub_parser.add_parser('configure', help='Try "./ns3 configure --help" for more configuration options') parser_configure.add_argument('configure', @@ -83,7 +93,6 @@ def parse_args(argv): parser_configure = on_off_argument(parser_configure, "des-metrics", "Logging all events in a json file with the name of the executable " "(which must call CommandLine::Parse(argc, argv)") - parser_configure = on_off_argument(parser_configure, "documentation", "documentation targets") parser_configure = on_off_argument(parser_configure, "examples", "the ns-3 examples") parser_configure = on_off_argument(parser_configure, "gcov", "code coverage analysis") parser_configure = on_off_argument(parser_configure, "gtk", "GTK support in ConfigStore") @@ -94,11 +103,12 @@ def parse_args(argv): "address, memory leaks and undefined behavior sanitizers") parser_configure = on_off_argument(parser_configure, "static", "Build a single static library with all ns-3", "Restore the shared libraries") + parser_configure = on_off_argument(parser_configure, "sudo", "use of sudo to setup suid bits on ns3 executables.") parser_configure = on_off_argument(parser_configure, "verbose", "printing of additional build system messages") parser_configure = on_off_argument(parser_configure, "warnings", "compiler warnings") parser_configure = on_off_argument(parser_configure, "werror", "Treat compiler warnings as errors", "Treat compiler warnings as warnings") - + parser_configure = on_off_argument(parser_configure, "visualizer", "the visualizer module") parser_configure.add_argument('--enable-modules', help='List of modules to build (e.g. core;network;internet)', action="store", type=str, default=None) @@ -140,16 +150,6 @@ def parse_args(argv): help="Do not execute the commands", action="store_true", default=None, dest="configure_dry_run") - parser_build = sub_parser.add_parser('build', - help=('Accepts a list of targets to build,' - ' or builds the entire project if no target is given')) - parser_build.add_argument('build', - help='Build the entire project or the specified target and dependencies', - action="store", nargs='*', default=None) - parser_build.add_argument('--dry-run', - help="Do not execute the commands", - action="store_true", default=None, dest="build_dry_run") - parser_clean = sub_parser.add_parser('clean', help='Removes files created by waf and ns3') parser_clean.add_argument('clean', nargs="?", @@ -167,6 +167,56 @@ def parse_args(argv): nargs="?", action="store", default=True) + parser_run = sub_parser.add_parser('run', + help='Try "./ns3 run --help" for more runtime options') + parser_run.add_argument('run', + help='Build and run executable. If --no-build is present, build step is skipped.', + type=str, default='') + parser_run.add_argument('--no-build', + help='Skip build step.', + action="store_true", default=False) + parser_run.add_argument('--command-template', + help=('Template of the command used to run the program given by run;' + ' It should be a shell command string containing %s inside,' + ' which will be replaced by the actual program.'), + type=str, default=None) + parser_run.add_argument('--cwd', + help='Set the working directory for a program.', + action="store", type=str, default=None) + parser_run.add_argument('--gdb', + help='Change the default command template to run programs with gdb', + action="store_true", default=None) + parser_run.add_argument('--valgrind', + help='Change the default command template to run programs with valgrind', + action="store_true", default=None) + parser_run.add_argument('--visualize', + help='Modify --run arguments to enable the visualizer', + action="store_true", default=None) + parser_run.add_argument('--dry-run', + help="Do not execute the commands", + action="store_true", default=None, dest="run_dry_run") + parser_run.add_argument('--enable-sudo', + help='Use sudo to setup suid bits on ns3 executables.', + dest='enable_sudo', action='store_true', + default=False) + + parser_shell = sub_parser.add_parser('shell', + help='Try "./ns3 shell --help" for more runtime options') + parser_shell.add_argument('shell', + nargs="?", + help='Export necessary environment variables and open a shell', + action="store", default=True) + + parser_docs = sub_parser.add_parser('docs', + help='Try "./ns3 docs --help" for more documentation options') + parser_docs.add_argument('docs', + help='Build project documentation', + choices=["manual", "models", "tutorial", "sphinx", "doxygen-no-build", "doxygen", "all"], + action="store", type=str, default=None) + + parser.add_argument('-j', '--jobs', + help='Set number of parallel jobs', + action='store', type=int, dest="jobs", default=max(1, os.cpu_count() - 1)) parser.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="dry_run") @@ -174,69 +224,14 @@ def parse_args(argv): help='Print the current configuration.', action="store_true", default=None) - parser.add_argument('--cwd', - help='Set the working directory for a program.', - action="store", type=str, default=None) - parser.add_argument('--no-task-lines', + parser.add_argument('--quiet', help="Don't print task lines, i.e. messages saying which tasks are being executed.", action="store_true", default=None) - parser.add_argument('--run', - help=('Run a locally built program; argument can be a program name,' - ' or a command starting with the program name.'), - type=str, default='') - parser.add_argument('--run-no-build', - help=( - 'Run a locally built program without rebuilding the project; ' - 'argument can be a program name, or a command starting with the program name.'), - type=str, default='') - parser.add_argument('--visualize', - help='Modify --run arguments to enable the visualizer', - action="store_true", default=None) - parser.add_argument('--command-template', - help=('Template of the command used to run the program given by --run;' - ' It should be a shell command string containing %s inside,' - ' which will be replaced by the actual program.'), - type=str, default=None) - parser.add_argument('--pyrun', - help=('Run a python program using locally built ns3 python module;' - ' argument is the path to the python program, optionally followed' - ' by command-line options that are passed to the program.'), - type=str, default='') - parser.add_argument('--pyrun-no-build', - help=( - 'Run a python program using locally built ns3 python module without rebuilding the project;' - ' argument is the path to the python program, optionally followed' - ' by command-line options that are passed to the program.'), - type=str, default='') - parser.add_argument('--gdb', - help='Change the default command template to run programs and unit tests with gdb', - action="store_true", default=None) - parser.add_argument('--valgrind', - help='Change the default command template to run programs and unit tests with valgrind', - action="store_true", default=None) - parser.add_argument('-j', '--jobs', - help='Set number of parallel jobs', - action='store', type=int, dest="jobs", default=max(1, os.cpu_count() - 1)) - # parser.add_argument('--shell', - # help=('DEPRECATED (run ./waf shell)'), - # action="store_true", default=None) - # parser.add_argument('--enable-sudo', - # help=('Use sudo to setup suid bits on ns3 executables.'), - # dest='enable_sudo', action='store_true', - # default=None) - parser.add_argument('--check', help='DEPRECATED (run ./test.py)', action='store_true', default=None) - parser.add_argument('--doxygen', - help='Run doxygen to generate html documentation from source comments.', - action="store_true", default=None) - parser.add_argument('--doxygen-no-build', - help=('Run doxygen to generate html documentation from source comments, ' - 'but do not wait for ns-3 to finish the full build.'), - action="store_true", default=None) # parser.add_argument('--docset', # help=( # 'Create Docset, without building. This requires the docsetutil tool from Xcode 9.2 or earlier.' @@ -248,9 +243,16 @@ def parse_args(argv): # Merge dry_runs dry_run_args = [(args.__getattribute__(name) if name in args else None) for name in - ["build_dry_run", "clean_dry_run", "configure_dry_run", "dry_run"]] + ["build_dry_run", "clean_dry_run", "configure_dry_run", "dry_run", "run_dry_run"]] args.dry_run = dry_run_args.count(True) > 0 + # If some positional options are not in args, set them to false. + for option in ["clean", "configure", "docs", "install", "run", "shell", "uninstall"]: + if option not in args: + setattr(args, option, False) + + if args.run and args.enable_sudo is None: + args.enable_sudo = True return args @@ -261,6 +263,7 @@ def check_build_profile(output_directory): ns3_version = None ns3_modules = None ns3_modules_tests = [] + enable_sudo = False if output_directory and os.path.exists(c4che_path): c4che_info = {} exec(open(c4che_path).read(), globals(), c4che_info) @@ -268,7 +271,9 @@ def check_build_profile(output_directory): ns3_version = c4che_info["VERSION"] ns3_modules = c4che_info["NS3_ENABLED_MODULES"] ns3_modules_tests = [x + "-test" for x in ns3_modules] - return build_profile, ns3_version, ns3_modules + ns3_modules_tests if ns3_modules else None + if "ENABLE_SUDO" in c4che_info: + enable_sudo = c4che_info["ENABLE_SUDO"] + return build_profile, ns3_version, ns3_modules + ns3_modules_tests if ns3_modules else None, enable_sudo def print_and_buffer(message): @@ -414,7 +419,7 @@ def configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_gener options = (("ASSERT", "asserts"), ("COVERAGE", "gcov"), ("DES_METRICS", "des_metrics"), - ("DOCS", "documentation"), + ("ENABLE_SUDO", "sudo"), ("EXAMPLES", "examples"), ("GTK3", "gtk"), ("LOG", "logs"), @@ -423,6 +428,7 @@ def configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_gener ("STATIC", "static"), ("TESTS", "tests"), ("VERBOSE", "verbose"), + ("VISUALIZER", "visualizer"), ("WARNINGS", "warnings"), ("WARNINGS_AS_ERRORS", "werror"), ) @@ -450,10 +456,6 @@ def configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_gener if args.prefix is not None: cmake_args.append("-DCMAKE_INSTALL_PREFIX=%s" % args.prefix) - # Build and link visualizer - if args.visualize is not None: - cmake_args.append("-DNS3_VISUALIZER=%s" % on_off(args.visualize)) - # Process enabled/disabled modules if args.enable_modules: cmake_args.append("-DNS3_ENABLED_MODULES=%s" % args.enable_modules) @@ -478,7 +480,9 @@ def configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_gener # Run cmake if not dry_run: - subprocess.run(cmake_args, cwd=current_cmake_cache_folder, stdout=output) + ret = subprocess.run(cmake_args, cwd=current_cmake_cache_folder, stdout=output) + if ret.returncode != 0: + exit(ret.returncode) def get_program_shortcuts(build_profile, ns3_version): @@ -522,7 +526,7 @@ def cmake_build(current_cmake_cache_folder, output, jobs, target=None, dry_run=F raise Exception("CMake was not found") version = re.findall("version (.*)\n", subprocess.check_output([cmake, "--version"]).decode("utf-8"))[0] - # Older CMake versions do not accept the number of jobs directly + # Older CMake versions don't accept the number of jobs directly jobs_part = ("-j %d" % jobs) if version >= "3.12.0" else "" target_part = (" --target %s" % target) if target else "" cmake_build_command = "cmake --build . %s%s" % (jobs_part, target_part) @@ -654,14 +658,16 @@ def build_step(args, output=output, dry_run=args.dry_run ) - if "build" in args: - # We can exit early if only building - exit(0) # If we are building specific targets, we build them one by one if "build" in args: - non_executable_targets = ["doxygen", + non_executable_targets = ["docs", + "doxygen", "doxygen-no-build", + "sphinx", + "manual", + "models", + "tutorial", "install", "uninstall", "cmake-format"] @@ -671,13 +677,21 @@ def build_step(args, target = "lib" + target elif target not in non_executable_targets: target = get_target_to_build(target, ns3_version, build_profile) + else: + # Sphinx target should have the sphinx prefix + if target in ["manual", "models", "tutorial"]: + target = "sphinx_%s" % target + + # Docs should build both doxygen and sphinx based docs + if target == "docs": + target = "sphinx" + args.build.append("doxygen") + cmake_build(current_cmake_cache_folder, jobs=args.jobs, target=target, output=output, dry_run=args.dry_run) - # We can also exit earlier in this case - exit(0) # The remaining case is when we want to build something to run if build_and_run: @@ -692,44 +706,61 @@ def build_step(args, def run_step(args, target_to_run, target_args): libdir = "%s/lib" % out_dir path_sep = ";" if sys.platform == "win32" else ":" - proc_env = {"PATH": os.getenv("PATH") + path_sep + libdir, - "PYTHON_PATH": "%s/bindings/python" % out_dir, - } + + custom_env = {"PATH": libdir, + "PYTHONPATH": "%s/bindings/python" % out_dir, + } if sys.platform != "win32": - proc_env["LD_LIBRARY_PATH"] = libdir + custom_env["LD_LIBRARY_PATH"] = libdir + + proc_env = os.environ.copy() + for (key, value) in custom_env.items(): + if key in proc_env: + proc_env[key] += path_sep + value + else: + proc_env[key] = value - # running from ns-3-dev (ns3_path) or cwd - working_dir = args.cwd if args.cwd else ns3_path debugging_software = [] - - # running valgrind? - if args.valgrind: - debugging_software.append(shutil.which("valgrind")) - - # running gdb? - if args.gdb: - debugging_software.extend([shutil.which("gdb"), "--args"]) - - # running with visualizer? - if args.visualize: - target_args.append("--SimulatorImplementationType=ns3::VisualSimulatorImpl") + working_dir = ns3_path + use_shell = False # running test.py/check? if args.check: target_to_run = os.sep.join([ns3_path, "test.py"]) - target_args = ["--nowaf", "--jobs=%d" % args.jobs] + target_args = ["--no-build", "--jobs=%d" % args.jobs] + elif args.shell: + target_to_run = "bash" + use_shell = True + else: + # running from ns-3-dev (ns3_path) or cwd + if args.cwd: + working_dir = args.cwd - # running with command template? - if args.command_template: - commands = (args.command_template % target_to_run).split() - target_to_run = commands[0] - target_args = commands[1:] + target_args + # running valgrind? + if args.valgrind: + debugging_software.append(shutil.which("valgrind")) + + # running gdb? + if args.gdb: + debugging_software.extend([shutil.which("gdb"), "--args"]) + + # running with the visualizer? + if args.visualize: + target_args.append("--SimulatorImplementationType=ns3::VisualSimulatorImpl") + + # running with command template? + if args.command_template: + commands = (args.command_template % target_to_run).split() + target_to_run = commands[0] + target_args = commands[1:] + target_args + if target_to_run in ["mpiexec", "mpirun"]: + target_args = ["--allow-run-as-root"] + target_args program_arguments = [*debugging_software, target_to_run, *target_args] if not run_only or args.dry_run: exported_variables = "export " - for (variable, value) in proc_env.items(): + for (variable, value) in custom_env.items(): if variable == "PATH": value = "$PATH" + path_sep + libdir exported_variables += "%s=%s " % (variable, value) @@ -741,18 +772,99 @@ def run_step(args, target_to_run, target_args): if not args.dry_run: try: - subprocess.run(program_arguments, env=proc_env, cwd=working_dir) + ret = subprocess.run(program_arguments, env=proc_env, cwd=working_dir, shell=use_shell) + exit(ret.returncode) except KeyboardInterrupt: print("Process was interrupted by the user") +# Debugging this with PyCharm is a no no. It refuses to work hanging indefinitely +def sudo_command(command: list, password: str): + # Run command and feed the sudo password + proc = subprocess.Popen(['sudo', '-S', *command], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ).communicate(input=password.encode() + b'\n') + stdout, stderr = proc[0].decode(), proc[1].decode() + + # Clean sudo password after each command + subprocess.Popen(["sudo", "-k"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # Check if the password is wrong + if "try again" in stderr: + raise Exception("Incorrect sudo password") + + return stdout, stderr + + +def sudo_step(args, target_to_run, configure_post_build: set): + # Check if sudo exists + sudo = shutil.which("sudo") + if not sudo: + raise Exception("Sudo is required by --enable-sudo, but it was not found") + + # We do this for specified targets if --enable-sudo was set in the run sub-parser + # And to all executables if set in the configure sub-parser + targets_to_sudo = configure_post_build + if target_to_run: + targets_to_sudo.add(target_to_run) + + password = os.getenv("SUDO_PASSWORD", None) + if not args.dry_run: + if password is None: + from getpass import getpass + password = getpass(prompt="Sudo password:") + + import stat + for target in targets_to_sudo: + # Check if the file was already built + if not os.path.exists(target): + continue + + # Check if we need to set anything + fstat = os.stat(target) + if (fstat.st_mode & stat.S_ISUID) == stat.S_ISUID: + continue + + # Log commands + relative_path_to_target = os.path.relpath(target, ns3_path) + chown_command = "chown root {}".format(relative_path_to_target) + chmod_command = "chmod u+s {}".format(relative_path_to_target) + print_and_buffer("; ".join([chown_command, chmod_command])) + + # Change permissions + if not args.dry_run: + out, err = sudo_command(chown_command.split(), password) + if len(out) > 0: + raise Exception("Failed to chown: ", relative_path_to_target) + + out, err = sudo_command(chmod_command.split(), password) + if len(out) > 0: + raise Exception("Failed to chmod: ", relative_path_to_target) + return + + +def refuse_run_as_root(): + # Check if the user is root and refuse to run + username = os.getenv("USER", "") + if username == "root": + raise Exception("Refusing to run as root. --enable-sudo will request your password when needed") + + def main(): global out_dir, run_only + # Refuse to run with sudo + refuse_run_as_root() + # Parse arguments args = parse_args(sys.argv[1:]) atexit.register(exit_handler, dry_run=args.dry_run) - output = subprocess.DEVNULL if args.no_task_lines else None + output = subprocess.DEVNULL if args.quiet else None # no arguments were passed, so can't possibly be reconfiguring anything, then we refresh and rebuild if len(sys.argv) == 1: @@ -763,36 +875,37 @@ def main(): exec(open(lock_file).read(), globals()) # Clean project if needed - if "clean" in args and args.clean: + if args.clean: clean_cmake_artifacts(dry_run=args.dry_run) # We end things earlier when cleaning return - # Doxygen-no-build requires print-introspected-doxygen, but it has no explicit dependencies, - # differently from the doxygen target - if args.doxygen: - args.build = ['doxygen'] - - if args.doxygen_no_build: - args.build = ['doxygen-no-build'] + # Docs options become cmake targets + if args.docs: + args.build = [args.docs] if args.docs != "all" else ["sphinx", "doxygen"] # Installation and uninstallation options become cmake targets - if "install" in args: + if args.install: args.build = ['install'] - if 'uninstall' in args: + if args.uninstall: args.build = ['uninstall'] # Get build profile - build_profile, ns3_version, ns3_modules = check_build_profile(out_dir) + build_profile, ns3_version, ns3_modules, enable_sudo = check_build_profile(out_dir) # Check if running something or reconfiguring ns-3 - run_only = args.run_no_build or args.pyrun_no_build - build_and_run = args.run or args.pyrun + run_only = False + build_and_run = False + if args.run: + if not args.no_build: + build_and_run = True + else: + run_only = True target_to_run = None target_args = [] current_cmake_cache_folder = None if not args.check and (run_only or build_and_run): - target_to_run = max(args.run_no_build, args.pyrun_no_build, args.run, args.pyrun) + target_to_run = args.run if len(target_to_run) > 0: # While testing a weird case appeared where the target to run is between quotes, # so we remove in case they exist @@ -812,7 +925,7 @@ def main(): # We end things earlier if only checking the current project configuration return - if "configure" in args: + if args.configure: configuration_step(current_cmake_cache_folder, current_cmake_generator, args, @@ -869,22 +982,27 @@ def main(): target_path = os.sep.join(target_path) return target_path - target_to_run = remove_overlapping_path(out_dir, target_to_run) + if not args.check and not args.shell and target_to_run: + target_to_run = remove_overlapping_path(out_dir, target_to_run) - # Waf doesn't add version prefix and build type suffix to the scratches, so we remove them - if current_cmake_cache_folder is None: - if "scratch" in target_to_run and run_only: - waf_target_to_run = target_to_run.replace(os.path.basename(target_to_run), run_only) - if os.path.exists(os.sep.join([out_dir, waf_target_to_run])): - target_to_run = waf_target_to_run - target_to_run = os.sep.join([out_dir, target_to_run]) + # Waf doesn't add version prefix and build type suffix to the scratches, so we remove them + if current_cmake_cache_folder is None: + if "scratch" in target_to_run and run_only: + waf_target_to_run = target_to_run.replace(os.path.basename(target_to_run), args.run) + if os.path.exists(os.sep.join([out_dir, waf_target_to_run])): + target_to_run = waf_target_to_run + target_to_run = os.sep.join([out_dir, target_to_run]) - # If we're only trying to run the target, we need to check if it actually exists first - if (run_only or build_and_run) and not os.path.exists(target_to_run): - raise Exception("Executable has not been built yet") + # If we're only trying to run the target, we need to check if it actually exists first + if (run_only or build_and_run) and not os.path.exists(target_to_run): + raise Exception("Executable has not been built yet") + + # Setup program as sudo + if enable_sudo or (args.run and args.enable_sudo): + sudo_step(args, target_to_run, set(ns3_programs.values()) if enable_sudo else set()) # Finally, we try to run it - if args.check or run_only or build_and_run: + if args.check or args.shell or run_only or build_and_run: run_step(args, target_to_run, target_args) return diff --git a/src/config-store/CMakeLists.txt b/src/config-store/CMakeLists.txt index 17bd967e6..5811c85ac 100644 --- a/src/config-store/CMakeLists.txt +++ b/src/config-store/CMakeLists.txt @@ -8,6 +8,9 @@ if(${GTK3_FOUND}) set(gtk3_headers model/gtk-config-store.h) include_directories(${GTK3_INCLUDE_DIRS} ${HarfBuzz_INCLUDE_DIRS}) set(gtk_libraries ${GTK3_LIBRARIES}) + if(${GCC}) + add_definitions(-Wno-parentheses) + endif() endif() if(${LIBXML2_FOUND}) diff --git a/src/core/model/command-line.cc b/src/core/model/command-line.cc index 79effd240..3fa388bf6 100644 --- a/src/core/model/command-line.cc +++ b/src/core/model/command-line.cc @@ -436,7 +436,7 @@ CommandLine::PrintDoxygenUsage (void) const os << "/**\n \\file " << m_shortName << ".cc\n" << "

Usage

\n" - << "$ ./ns3 --run \"" << m_shortName + << "$ ./ns3 run \"" << m_shortName << (m_options.size () ? " [Program Options]" : "") << (nonOptions.size () ? " [Program Arguments]" : "") << "\"\n"; diff --git a/src/core/model/example-as-test.cc b/src/core/model/example-as-test.cc index f3a2a91fd..6e42ed539 100644 --- a/src/core/model/example-as-test.cc +++ b/src/core/model/example-as-test.cc @@ -87,8 +87,8 @@ ExampleAsTestCase::DoRun (void) std::stringstream ss; // Use bash as shell to allow use of PIPESTATUS - ss << "bash -c './ns3 --run-no-build " << m_program - << " --command-template=\"" << GetCommandTemplate () << "\"" + ss << "bash -c './ns3 run " << m_program + << " --no-build --command-template=\"" << GetCommandTemplate () << "\"" // redirect std::clog, std::cerr to std::cout << " 2>&1 " diff --git a/src/fd-net-device/CMakeLists.txt b/src/fd-net-device/CMakeLists.txt index 8673bb4a6..6016266ce 100644 --- a/src/fd-net-device/CMakeLists.txt +++ b/src/fd-net-device/CMakeLists.txt @@ -135,9 +135,8 @@ if(${ENABLE_FDNETDEV}) model/fd-net-device.cc ) - set(header_files - ${tap_headers} ${emu_headers} ${dpdk_headers} - model/fd-net-device.h helper/fd-net-device-helper.h + set(header_files ${tap_headers} ${emu_headers} ${dpdk_headers} + model/fd-net-device.h helper/fd-net-device-helper.h ) set(libraries_to_link ${libnetwork} ${LIB_AS_NEEDED_PRE} ${DPDK_LIBRARIES} diff --git a/src/stats/CMakeLists.txt b/src/stats/CMakeLists.txt index 67ec15ae3..a8ecb6316 100644 --- a/src/stats/CMakeLists.txt +++ b/src/stats/CMakeLists.txt @@ -1,7 +1,7 @@ set(name stats) if(${NS3_SQLITE}) - find_package(SQLite3) + find_package(SQLite3 QUIET) check_include_file_cxx(semaphore.h HAVE_SEMAPHORE_H) if(${SQLite3_FOUND}) set(sqlite_sources model/sqlite-data-output.cc) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index b960c4e10..da7ab4f27 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -32,7 +32,11 @@ if(network IN_LIST libs_to_build) ) add_executable(print-introspected-doxygen print-introspected-doxygen.cc) - target_link_libraries(print-introspected-doxygen ${libnetwork}) + target_link_libraries( + print-introspected-doxygen + PRIVATE ${LIB_AS_NEEDED_PRE} ${ns3-libs} ${ns3-contrib-libs} + ${LIB_AS_NEEDED_POST} + ) set_runtime_outputdirectory( print-introspected-doxygen ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/utils/ "" ) diff --git a/utils/tests/test-ns3.py b/utils/tests/test-ns3.py index 536b7e50b..6a9251955 100644 --- a/utils/tests/test-ns3.py +++ b/utils/tests/test-ns3.py @@ -18,6 +18,10 @@ # # Author: Gabriel Ferreira +"""! +Test suite for the ns3 wrapper script +""" + import glob import os import re @@ -36,8 +40,8 @@ usual_build_status_script = os.sep.join([usual_outdir, "build-status.py"]) usual_c4che_script = os.sep.join([usual_outdir, "c4che", "_cache.py"]) usual_lib_outdir = os.sep.join([usual_outdir, "lib"]) -# Move the current working directory to the ns-3-dev/utils/tests folder -os.chdir(os.path.dirname(os.path.abspath(__file__))) +# Move the current working directory to the ns-3-dev folder +os.chdir(ns3_path) # Cmake commands cmake_build_project_command = "cmake --build . -j".format(ns3_path=ns3_path) @@ -46,24 +50,26 @@ cmake_build_target_command = partial("cmake --build . -j {jobs} --target {target ) -def run_ns3(args): - """ +def run_ns3(args, env=None): + """! Runs the ns3 wrapper script with arguments - :param args: string containing arguments that will get split before calling ns3 - :return: tuple containing (error code, stdout and stderr) + @param args: string containing arguments that will get split before calling ns3 + @param env: environment variables dictionary + @return tuple containing (error code, stdout and stderr) """ - return run_program(ns3_script, args, True) + return run_program(ns3_script, args, python=True, env=env) # Adapted from https://github.com/metabrainz/picard/blob/master/picard/util/__init__.py -def run_program(program, args, python=False, cwd=ns3_path): - """ +def run_program(program, args, python=False, cwd=ns3_path, env=None): + """! Runs a program with the given arguments and returns a tuple containing (error code, stdout and stderr) - :param program: program to execute (or python script) - :param args: string containing arguments that will get split before calling the program - :param python: flag indicating whether the program is a python script - :param cwd: working directory used that will be the root folder for the execution - :return: tuple containing (error code, stdout and stderr) + @param program: program to execute (or python script) + @param args: string containing arguments that will get split before calling the program + @param python: flag indicating whether the program is a python script + @param cwd: the working directory used that will be the root folder for the execution + @param env: environment variables dictionary + @return tuple containing (error code, stdout and stderr) """ if type(args) != str: raise Exception("args should be a string") @@ -80,24 +86,31 @@ def run_program(program, args, python=False, cwd=ns3_path): for i in range(len(arguments)): arguments[i] = arguments[i].replace("\"", "") + # Forward environment variables used by the ns3 script + current_env = os.environ.copy() + + # Add different environment variables + if env: + current_env.update(env) + # Call program with arguments ret = subprocess.run( arguments, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=cwd # run process from the ns-3-dev path + cwd=cwd, # run process from the ns-3-dev path + env=current_env ) - # Return (error code, stdout and stderr) return ret.returncode, ret.stdout.decode(sys.stdout.encoding), ret.stderr.decode(sys.stderr.encoding) def get_programs_list(build_status_script_path=usual_build_status_script): - """ + """! Extracts the programs list from build-status.py - :param build_status_script_path: path containing build-status.py - :return: list of programs + @param build_status_script_path: path containing build-status.py + @return list of programs. """ values = {} with open(build_status_script_path) as f: @@ -106,40 +119,40 @@ def get_programs_list(build_status_script_path=usual_build_status_script): def get_libraries_list(lib_outdir=usual_lib_outdir): - """ + """! Gets a list of built libraries - :param lib_outdir: path containing libraries - :return: list of built libraries + @param lib_outdir: path containing libraries + @return list of built libraries. """ return glob.glob(lib_outdir + '/*', recursive=True) def get_headers_list(outdir=usual_outdir): - """ + """! Gets a list of header files - :param outdir: path containing headers - :return: list of headers + @param outdir: path containing headers + @return list of headers. """ return glob.glob(outdir + '/**/*.h', recursive=True) def read_c4che_entry(entry, c4che_script_path=usual_c4che_script): - """ + """! Read interesting entries from the c4che/_cache.py file - :param entry: entry to read from c4che/_cache.py - :param c4che_script_path: path containing _cache.py - :return: value of the requested entry + @param entry: entry to read from c4che/_cache.py + @param c4che_script_path: path containing _cache.py + @return value of the requested entry. """ values = {} with open(c4che_script_path) as f: exec(f.read(), globals(), values) - return values[entry] + return values.get(entry, None) def get_test_enabled(): - """ + """! Check if tests are enabled in the c4che/_cache.py - :return: bool + @return bool. """ return read_c4che_entry("ENABLE_TESTS") @@ -147,18 +160,23 @@ def get_test_enabled(): def get_enabled_modules(): """ Check if tests are enabled in the c4che/_cache.py - :return: list of enabled modules (prefixed with 'ns3-') + @return list of enabled modules (prefixed with 'ns3-'). """ return read_c4che_entry("NS3_ENABLED_MODULES") class NS3RunWafTargets(unittest.TestCase): + """! + ns3 tests related to compatibility with Waf-produced binaries + """ + ## when cleaned_once is False, clean up build artifacts and reconfigure cleaned_once = False def setUp(self): - """ + """! Clean the default build directory, then configure and build ns-3 with waf + @return None """ if not NS3RunWafTargets.cleaned_once: NS3RunWafTargets.cleaned_once = True @@ -172,90 +190,151 @@ class NS3RunWafTargets(unittest.TestCase): self.assertIn("finished successfully", stdout) def test_01_loadExecutables(self): - # Check if build-status.py exists, then read to get list of executables + """! + Try to load executables built by waf + @return None + """ + # Check if build-status.py exists, then read to get list of executables. self.assertTrue(os.path.exists(usual_build_status_script)) + ## ns3_executables holds a list of executables in build-status.py self.ns3_executables = get_programs_list() self.assertGreater(len(self.ns3_executables), 0) def test_02_loadModules(self): - # Check if c4che.py exists than read to get the list of enabled modules + """! + Try to load modules built by waf + @return None + """ + # Check if c4che.py exists than read to get the list of enabled modules. self.assertTrue(os.path.exists(usual_c4che_script)) + ## ns3_modules holds a list to the modules enabled stored in c4che.py self.ns3_modules = get_enabled_modules() self.assertGreater(len(self.ns3_modules), 0) def test_03_runNobuildScratchSim(self): - return_code, stdout, stderr = run_ns3("--run-no-build scratch-simulator") + """! + Try to run an executable built by waf + @return None + """ + return_code, stdout, stderr = run_ns3("run scratch-simulator --no-build") self.assertEqual(return_code, 0) self.assertIn("Scratch Simulator", stderr) def test_04_runNobuildExample(self): - return_code, stdout, stderr = run_ns3("--run-no-build command-line-example") + """! + Try to run a different executable built by waf + @return None + """ + return_code, stdout, stderr = run_ns3("run command-line-example --no-build") self.assertEqual(return_code, 0) self.assertIn("command-line-example", stdout) def test_05_runTestCaseCoreExampleSimulator(self): - return_code, stdout, stderr = run_program("test.py", "--nowaf -s core-example-simulator", True) + """! + Try to run a test case built by waf that calls the ns3 wrapper script + @return None + """ + return_code, stdout, stderr = run_program("test.py", "--no-build -s core-example-simulator", True) self.assertEqual(return_code, 0) self.assertIn("PASS", stdout) def test_06_runTestCaseExamplesAsTestsTestSuite(self): - return_code, stdout, stderr = run_program("test.py", "--nowaf -s examples-as-tests-test-suite", True) + """! + Try to run a different test case built by waf that calls the ns3 wrapper script + @return None + """ + return_code, stdout, stderr = run_program("test.py", "--no-build -s examples-as-tests-test-suite", True) self.assertEqual(return_code, 0) self.assertIn("PASS", stdout) def test_07_runCoreExampleSimulator(self): + """! + Try to run test cases built by waf that calls the ns3 wrapper script + when the output directory is set to a different path + @return None + """ run_ns3("clean") - return_code, stdout, stderr = run_program("waf", "configure --enable-examples --enable-tests --out build/debug", python=True) + return_code, stdout, stderr = run_program("waf", + "configure --enable-examples --enable-tests --out build/debug", + python=True) self.assertEqual(return_code, 0) self.assertIn("finished successfully", stdout) - return_code, stdout, stderr = run_program("waf", '--run "test-runner --suite=core-example-simulator --verbose"', True) + return_code, stdout, stderr = run_program("waf", + '--run "test-runner --suite=core-example-simulator --verbose"', + True) self.assertEqual(return_code, 0) self.assertIn("PASS", stdout) class NS3CommonSettingsTestCase(unittest.TestCase): + """! + ns3 tests related to generic options + """ + def setUp(self): - """ + """! Clean configuration/build artifacts before common commands + @return None """ super().setUp() - # No special setup for common test cases other than making sure we are working on a clean directory + # No special setup for common test cases other than making sure we are working on a clean directory. run_ns3("clean") def test_01_NoOption(self): + """! + Test not passing any arguments to + @return None + """ return_code, stdout, stderr = run_ns3("") self.assertEqual(return_code, 0) self.assertIn("You need to configure ns-3 first: try ./ns3 configure", stdout) def test_02_NoTaskLines(self): - return_code, stdout, stderr = run_ns3("--no-task-lines") + """! + Test only passing --quiet argument to ns3 + @return None + """ + return_code, stdout, stderr = run_ns3("--quiet") self.assertEqual(return_code, 0) self.assertIn("You need to configure ns-3 first: try ./ns3 configure", stdout) def test_03_CheckConfig(self): + """! + Test only passing --check-config argument to ns3 + @return None + """ return_code, stdout, stderr = run_ns3("--check-config") self.assertEqual(return_code, 1) self.assertIn("Project was not configured", stderr) class NS3ConfigureBuildProfileTestCase(unittest.TestCase): + """! + ns3 tests related to build profiles + """ + def setUp(self): - """ + """! Clean configuration/build artifacts before testing configuration settings + @return None """ super().setUp() - # No special setup for common test cases other than making sure we are working on a clean directory + # No special setup for common test cases other than making sure we are working on a clean directory. run_ns3("clean") def test_01_Debug(self): + """! + Test the debug build + @return None + """ return_code, stdout, stderr = run_ns3("configure -d debug --enable-verbose") self.assertEqual(return_code, 0) self.assertIn("Build profile : debug", stdout) self.assertIn("Build files have been written to", stdout) - # Build core to check if profile suffixes match the expected + # Build core to check if profile suffixes match the expected. return_code, stdout, stderr = run_ns3("build core") self.assertEqual(return_code, 0) self.assertIn("Built target libcore", stdout) @@ -265,12 +344,20 @@ class NS3ConfigureBuildProfileTestCase(unittest.TestCase): self.assertIn("core-debug", libraries[0]) def test_02_Release(self): + """! + Test the release build + @return None + """ return_code, stdout, stderr = run_ns3("configure -d release") self.assertEqual(return_code, 0) self.assertIn("Build profile : release", stdout) self.assertIn("Build files have been written to", stdout) def test_03_Optimized(self): + """! + Test the optimized build + @return None + """ return_code, stdout, stderr = run_ns3("configure -d optimized --enable-verbose") self.assertEqual(return_code, 0) self.assertIn("Build profile : optimized", stdout) @@ -286,64 +373,85 @@ class NS3ConfigureBuildProfileTestCase(unittest.TestCase): self.assertIn("core-optimized", libraries[0]) def test_04_Typo(self): + """! + Test a build type with a typo + @return None + """ return_code, stdout, stderr = run_ns3("configure -d Optimized") self.assertEqual(return_code, 2) self.assertIn("invalid choice: 'Optimized'", stderr) def test_05_TYPO(self): + """! + Test a build type with another typo + @return None + """ return_code, stdout, stderr = run_ns3("configure -d OPTIMIZED") self.assertEqual(return_code, 2) self.assertIn("invalid choice: 'OPTIMIZED'", stderr) class NS3BaseTestCase(unittest.TestCase): + """! + Generic test case with basic function inherited by more complex tests. + """ + ## when cleaned_once is False, clean up build artifacts and reconfigure cleaned_once = False def config_ok(self, return_code, stdout): - """ + """! Check if configuration for release mode worked normally - :param return_code: return code from CMake - :param stdout: output from CMake + @param return_code: return code from CMake + @param stdout: output from CMake. + @return None """ self.assertEqual(return_code, 0) self.assertIn("Build profile : release", stdout) self.assertIn("Build files have been written to", stdout) def setUp(self): - """ + """! Clean configuration/build artifacts before testing configuration and build settings After configuring the build as release, - check if configuration worked and check expected output files + check if configuration worked and check expected output files. + @return None """ super().setUp() if os.path.exists(ns3rc_script): os.remove(ns3rc_script) - # We only clear it once and then update the settings by changing flags or consuming ns3rc + # We only clear it once and then update the settings by changing flags or consuming ns3rc. if not NS3BaseTestCase.cleaned_once: NS3BaseTestCase.cleaned_once = True run_ns3("clean") return_code, stdout, stderr = run_ns3("configure -d release --enable-verbose") self.config_ok(return_code, stdout) - # Check if build-status.py exists, then read to get list of executables + # Check if build-status.py exists, then read to get list of executables. self.assertTrue(os.path.exists(usual_build_status_script)) + ## ns3_executables holds a list of executables in build-status.py self.ns3_executables = get_programs_list() - # Check if c4che.py exists than read to get the list of enabled modules + # Check if c4che.py exists than read to get the list of enabled modules. self.assertTrue(os.path.exists(usual_c4che_script)) + ## ns3_modules holds a list to the modules enabled stored in c4che.py self.ns3_modules = get_enabled_modules() class NS3ConfigureTestCase(NS3BaseTestCase): + """! + Test ns3 configuration options + """ + ## when cleaned_once is False, clean up build artifacts and reconfigure cleaned_once = False def setUp(self): - """ + """! Reuse cleaning/release configuration from NS3BaseTestCase if flag is cleaned + @return None """ if not NS3ConfigureTestCase.cleaned_once: NS3ConfigureTestCase.cleaned_once = True @@ -351,24 +459,32 @@ class NS3ConfigureTestCase(NS3BaseTestCase): super().setUp() def test_01_Examples(self): + """! + Test enabling and disabling examples + @return None + """ return_code, stdout, stderr = run_ns3("configure --enable-examples") - # This just tests if we didn't break anything, not that we actually have enabled anything + # This just tests if we didn't break anything, not that we actually have enabled anything. self.config_ok(return_code, stdout) - # If nothing went wrong, we should have more executables in the list after enabling the examples + # If nothing went wrong, we should have more executables in the list after enabling the examples. self.assertGreater(len(get_programs_list()), len(self.ns3_executables)) - # Now we disabled them back + # Now we disabled them back. return_code, stdout, stderr = run_ns3("configure --disable-examples") - # This just tests if we didn't break anything, not that we actually have enabled anything + # This just tests if we didn't break anything, not that we actually have enabled anything. self.config_ok(return_code, stdout) - # And check if they went back to the original list + # Then check if they went back to the original list. self.assertEqual(len(get_programs_list()), len(self.ns3_executables)) def test_02_Tests(self): + """! + Test enabling and disabling tests + @return None + """ # Try enabling tests return_code, stdout, stderr = run_ns3("configure --enable-tests") self.config_ok(return_code, stdout) @@ -387,11 +503,15 @@ class NS3ConfigureTestCase(NS3BaseTestCase): # Now building the library test should fail return_code, stdout, stderr = run_ns3("build core-test") - # And check if they went back to the original list + # Then check if they went back to the original list self.assertGreater(len(stderr), 0) def test_03_EnableModules(self): - # Try filtering enabled modules to network+wifi and their dependencies + """! + Test enabling specific modules + @return None + """ + # Try filtering enabled modules to network+Wi-Fi and their dependencies return_code, stdout, stderr = run_ns3("configure --enable-modules='network;wifi'") self.config_ok(return_code, stdout) @@ -401,68 +521,84 @@ class NS3ConfigureTestCase(NS3BaseTestCase): self.assertIn("ns3-network", enabled_modules) self.assertIn("ns3-wifi", enabled_modules) - # Try cleaning the list of enabled modules to reset to the normal configuration + # Try cleaning the list of enabled modules to reset to the normal configuration. return_code, stdout, stderr = run_ns3("configure --enable-modules=''") self.config_ok(return_code, stdout) - # At this point we should have the same amount of modules that we had when we started + # At this point we should have the same amount of modules that we had when we started. self.assertEqual(len(get_enabled_modules()), len(self.ns3_modules)) def test_04_DisableModules(self): - # Try filtering disabled modules to disable lte and modules that depend on it + """! + Test disabling specific modules + @return None + """ + # Try filtering disabled modules to disable lte and modules that depend on it. return_code, stdout, stderr = run_ns3("configure --disable-modules='lte;mpi'") self.config_ok(return_code, stdout) - # At this point we should have fewer modules + # At this point we should have fewer modules. enabled_modules = get_enabled_modules() self.assertLess(len(enabled_modules), len(self.ns3_modules)) self.assertNotIn("ns3-lte", enabled_modules) self.assertNotIn("ns3-mpi", enabled_modules) - # Try cleaning the list of enabled modules to reset to the normal configuration + # Try cleaning the list of enabled modules to reset to the normal configuration. return_code, stdout, stderr = run_ns3("configure --disable-modules=''") self.config_ok(return_code, stdout) - # At this point we should have the same amount of modules that we had when we started + # At this point we should have the same amount of modules that we had when we started. self.assertEqual(len(get_enabled_modules()), len(self.ns3_modules)) def test_05_EnableModulesComma(self): - # Try filtering enabled modules to network+wifi and their dependencies + """! + Test enabling comma-separated (waf-style) examples + @return None + """ + # Try filtering enabled modules to network+Wi-Fi and their dependencies. return_code, stdout, stderr = run_ns3("configure --enable-modules='network,wifi'") self.config_ok(return_code, stdout) - # At this point we should have fewer modules + # At this point we should have fewer modules. enabled_modules = get_enabled_modules() self.assertLess(len(get_enabled_modules()), len(self.ns3_modules)) self.assertIn("ns3-network", enabled_modules) self.assertIn("ns3-wifi", enabled_modules) - # Try cleaning the list of enabled modules to reset to the normal configuration + # Try cleaning the list of enabled modules to reset to the normal configuration. return_code, stdout, stderr = run_ns3("configure --enable-modules=''") self.config_ok(return_code, stdout) - # At this point we should have the same amount of modules that we had when we started + # At this point we should have the same amount of modules that we had when we started. self.assertEqual(len(get_enabled_modules()), len(self.ns3_modules)) def test_06_DisableModulesComma(self): - # Try filtering disabled modules to disable lte and modules that depend on it + """! + Test disabling comma-separated (waf-style) examples + @return None + """ + # Try filtering disabled modules to disable lte and modules that depend on it. return_code, stdout, stderr = run_ns3("configure --disable-modules='lte,mpi'") self.config_ok(return_code, stdout) - # At this point we should have fewer modules + # At this point we should have fewer modules. enabled_modules = get_enabled_modules() self.assertLess(len(enabled_modules), len(self.ns3_modules)) self.assertNotIn("ns3-lte", enabled_modules) self.assertNotIn("ns3-mpi", enabled_modules) - # Try cleaning the list of enabled modules to reset to the normal configuration + # Try cleaning the list of enabled modules to reset to the normal configuration. return_code, stdout, stderr = run_ns3("configure --disable-modules=''") self.config_ok(return_code, stdout) - # At this point we should have the same amount of modules that we had when we started + # At this point we should have the same amount of modules that we had when we started. self.assertEqual(len(get_enabled_modules()), len(self.ns3_modules)) def test_07_Ns3rc(self): + """! + Test loading settings from the ns3rc config file + @return None + """ ns3rc_template = "# ! /usr/bin/env python\ \ # A list of the modules that will be enabled when ns-3 is run.\ @@ -478,15 +614,15 @@ class NS3ConfigureTestCase(NS3BaseTestCase): tests_enabled = {tests}\ " - # Now we repeat the command line tests but with the ns3rc file + # Now we repeat the command line tests but with the ns3rc file. with open(ns3rc_script, "w") as f: f.write(ns3rc_template.format(modules="'lte'", examples="False", tests="True")) - # Reconfigure + # Reconfigure. return_code, stdout, stderr = run_ns3("configure") self.config_ok(return_code, stdout) - # Check + # Check. enabled_modules = get_enabled_modules() self.assertLess(len(get_enabled_modules()), len(self.ns3_modules)) self.assertIn("ns3-lte", enabled_modules) @@ -521,6 +657,10 @@ class NS3ConfigureTestCase(NS3BaseTestCase): self.assertEqual(len(get_programs_list()), len(self.ns3_executables)) def test_08_DryRun(self): + """! + Test dry-run (printing commands to be executed instead of running them) + @return None + """ run_ns3("clean") # Try dry-run before and after the positional commands (outputs should match) @@ -537,14 +677,14 @@ class NS3ConfigureTestCase(NS3BaseTestCase): run_ns3("build scratch-simulator") # Run all cases and then check outputs - return_code0, stdout0, stderr0 = run_ns3("--dry-run --run scratch-simulator") - return_code1, stdout1, stderr1 = run_ns3("--run scratch-simulator") - return_code2, stdout2, stderr2 = run_ns3("--dry-run --run-no-build scratch-simulator") - return_code3, stdout3, stderr3 = run_ns3("--run-no-build scratch-simulator") + return_code0, stdout0, stderr0 = run_ns3("--dry-run run scratch-simulator") + return_code1, stdout1, stderr1 = run_ns3("run scratch-simulator") + return_code2, stdout2, stderr2 = run_ns3("--dry-run run scratch-simulator --no-build") + return_code3, stdout3, stderr3 = run_ns3("run scratch-simulator --no-build ") - # Return code and stderr should be the same for all of them + # Return code and stderr should be the same for all of them. self.assertEqual(sum([return_code0, return_code1, return_code2, return_code3]), 0) - self.assertEqual([stderr0, stderr1, stderr2, stderr3], [""]*4) + self.assertEqual([stderr0, stderr1, stderr2, stderr3], [""] * 4) scratch_path = None for program in get_programs_list(): @@ -568,13 +708,45 @@ class NS3ConfigureTestCase(NS3BaseTestCase): # Case 3: run-no-build (should print the target output only) self.assertEqual("", stdout3) + def test_09_PropagationOfReturnCode(self): + """! + Test if ns3 is propagating back the return code from the executables called with the run command + @return None + """ + # From this point forward we are reconfiguring in debug mode + return_code, _, _ = run_ns3("clean") + self.assertEqual(return_code, 0) + + return_code, _, _ = run_ns3("configure --enable-examples --enable-tests") + self.assertEqual(return_code, 0) + + # Build necessary executables + return_code, stdout, stderr = run_ns3("build command-line-example test-runner") + self.assertEqual(return_code, 0) + + # Now some tests will succeed normally + return_code, stdout, stderr = run_ns3("run \"test-runner --test-name=command-line\" --no-build") + self.assertEqual(return_code, 0) + + # Now some tests will fail during NS_COMMANDLINE_INTROSPECTION + return_code, stdout, stderr = run_ns3("run \"test-runner --test-name=command-line\" --no-build", + env={"NS_COMMANDLINE_INTROSPECTION": ".."} + ) + self.assertNotEqual(return_code, 0) + class NS3BuildBaseTestCase(NS3BaseTestCase): + """! + Tests ns3 regarding building the project + """ + + ## when cleaned_once is False, clean up build artifacts and reconfigure cleaned_once = False def setUp(self): - """ + """! Reuse cleaning/release configuration from NS3BaseTestCase if flag is cleaned + @return None """ if not NS3BuildBaseTestCase.cleaned_once: NS3BuildBaseTestCase.cleaned_once = True @@ -584,16 +756,28 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): self.ns3_libraries = get_libraries_list() def test_01_BuildExistingTargets(self): + """! + Try building the core library + @return None + """ return_code, stdout, stderr = run_ns3("build core") self.assertEqual(return_code, 0) self.assertIn("Built target libcore", stdout) def test_02_BuildNonExistingTargets(self): - # tests are not enable, so the target isn't available + """! + Try building core-test library without tests enabled + @return None + """ + # tests are not enabled, so the target isn't available return_code, stdout, stderr = run_ns3("build core-test") self.assertGreater(len(stderr), 0) def test_03_BuildProject(self): + """! + Try building the project: + @return None + """ return_code, stdout, stderr = run_ns3("build") self.assertEqual(return_code, 0) self.assertIn("Built target", stdout) @@ -602,52 +786,64 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): self.assertIn(cmake_build_project_command, stdout) def test_04_BuildProjectNoTaskLines(self): - return_code, stdout, stderr = run_ns3("--no-task-lines build") + """! + Try hiding task lines + @return None + """ + return_code, stdout, stderr = run_ns3("--quiet build") self.assertEqual(return_code, 0) self.assertIn(cmake_build_project_command, stdout) def test_05_BreakBuild(self): - # change an essential file to break the build + """! + Try removing an essential file to break the build + @return None + """ + # change an essential file to break the build. attribute_cc_path = os.sep.join([ns3_path, "src", "core", "model", "attribute.cc"]) attribute_cc_bak_path = attribute_cc_path + ".bak" shutil.move(attribute_cc_path, attribute_cc_bak_path) - # build should break + # build should break. return_code, stdout, stderr = run_ns3("build") self.assertNotEqual(return_code, 0) - # move file back + # move file back. shutil.move(attribute_cc_bak_path, attribute_cc_path) - # build should work again + # build should work again. return_code, stdout, stderr = run_ns3("build") self.assertEqual(return_code, 0) def test_06_TestVersionFile(self): + """! + Test if changing the version file affects the library names + @return None + """ version_file = os.sep.join([ns3_path, "VERSION"]) with open(version_file, "w") as f: f.write("3-00\n") - # Reconfigure + # Reconfigure. return_code, stdout, stderr = run_ns3("configure") self.config_ok(return_code, stdout) - # Build + # Build. return_code, stdout, stderr = run_ns3("build") self.assertEqual(return_code, 0) self.assertIn("Built target", stdout) - # Programs with new versions + # Programs with new versions. new_programs = get_programs_list() - # Check if they exist + # Check if they exist. for program in new_programs: self.assertTrue(os.path.exists(program)) - # Check if we still have the same number of binaries + # Check if we still have the same number of binaries. self.assertEqual(len(new_programs), len(self.ns3_executables)) - # Check if versions changed from 3-dev to 3-00 + # Check if versions changed from 3-dev to 3-00. libraries = get_libraries_list() new_libraries = list(set(libraries).difference(set(self.ns3_libraries))) self.assertEqual(len(new_libraries), len(self.ns3_libraries)) @@ -656,26 +852,35 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): self.assertIn("libns3-00", library) self.assertTrue(os.path.exists(library)) - # Restore version file + # Restore version file. with open(version_file, "w") as f: f.write("3-dev\n") - # Reset flag to let it clean the build + # Reset flag to let it clean the build. NS3BuildBaseTestCase.cleaned_once = False def test_07_OutputDirectory(self): - # Re-build to return to the original state - return_code, stdout, stderr = run_ns3("build") + """! + Try setting a different output directory and if everything is + in the right place and still working correctly + @return None + """ + # Re-build to return to the original state. + run_ns3("build") + + ## ns3_libraries holds a list of built module libraries self.ns3_libraries = get_libraries_list() + + ## ns3_executables holds a list of executables in build-status.py self.ns3_executables = get_programs_list() - # Delete built programs and libraries to check if they were restored later + # Delete built programs and libraries to check if they were restored later. for program in self.ns3_executables: os.remove(program) for library in self.ns3_libraries: os.remove(library) - # Reconfigure setting the output folder to ns-3-dev/build/release (both as an absolute path or relative) + # Reconfigure setting the output folder to ns-3-dev/build/release (both as an absolute path or relative). absolute_path = os.sep.join([ns3_path, "build", "release"]) relative_path = os.sep.join(["build", "release"]) for different_out_dir in [absolute_path, relative_path]: @@ -684,90 +889,106 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): self.assertIn("Build directory : %s" % absolute_path, stdout) # Build - return_code, stdout, stderr = run_ns3("build") + run_ns3("build") - # Check if we have the same number of binaries and that they were built correctly + # Check if we have the same number of binaries and that they were built correctly. new_programs = get_programs_list(os.sep.join([absolute_path, "build-status.py"])) self.assertEqual(len(new_programs), len(self.ns3_executables)) for program in new_programs: self.assertTrue(os.path.exists(program)) - # Check if we have the same number of libraries and that they were built correctly + # Check if we have the same number of libraries and that they were built correctly. libraries = get_libraries_list(os.sep.join([absolute_path, "lib"])) new_libraries = list(set(libraries).difference(set(self.ns3_libraries))) self.assertEqual(len(new_libraries), len(self.ns3_libraries)) for library in new_libraries: self.assertTrue(os.path.exists(library)) - # Remove files in the different output dir + # Remove files in the different output dir. shutil.rmtree(absolute_path) - # Restore original output directory + # Restore original output directory. return_code, stdout, stderr = run_ns3("configure --out=''") self.config_ok(return_code, stdout) self.assertIn("Build directory : %s" % usual_outdir, stdout) - # Try re-building - return_code, stdout, stderr = run_ns3("build") + # Try re-building. + run_ns3("build") - # Check if we have the same binaries we had at the beginning + # Check if we have the same binaries we had at the beginning. new_programs = get_programs_list() self.assertEqual(len(new_programs), len(self.ns3_executables)) for program in new_programs: self.assertTrue(os.path.exists(program)) - # Check if we have the same libraries we had at the beginning + # Check if we have the same libraries we had at the beginning. libraries = get_libraries_list() self.assertEqual(len(libraries), len(self.ns3_libraries)) for library in libraries: self.assertTrue(os.path.exists(library)) def test_08_InstallationAndUninstallation(self): - # Remove existing libraries from the previous step + """! + Tries setting a ns3 version, then installing it. + After that, tries searching for ns-3 with CMake's find_package(ns3). + Finally, tries using core library in a 3rd-party project + @return None + """ + # Remove existing libraries from the previous step. libraries = get_libraries_list() for library in libraries: os.remove(library) - # 3-dev version format is not supported by CMake, so we use 3.01 + # 3-dev version format is not supported by CMake, so we use 3.01. version_file = os.sep.join([ns3_path, "VERSION"]) with open(version_file, "w") as f: f.write("3-01\n") - # Reconfigure setting the installation folder to ns-3-dev/build/install + # Reconfigure setting the installation folder to ns-3-dev/build/install. install_prefix = os.sep.join([ns3_path, "build", "install"]) return_code, stdout, stderr = run_ns3("configure --prefix=%s" % install_prefix) self.config_ok(return_code, stdout) - # Build - return_code, stdout, stderr = run_ns3("build") + # Build. + run_ns3("build") libraries = get_libraries_list() headers = get_headers_list() - # Install - return_code, stdout, stderr = run_ns3("install") + # Install. + run_ns3("install") - # Find out if libraries were installed to lib or lib64 (Fedora thing) + # Find out if libraries were installed to lib or lib64 (Fedora thing). lib64 = os.path.exists(os.sep.join([install_prefix, "lib64"])) installed_libdir = os.sep.join([install_prefix, ("lib64" if lib64 else "lib")]) - # Make sure all libraries were installed + # Make sure all libraries were installed. installed_libraries = get_libraries_list(installed_libdir) installed_libraries_list = ";".join(installed_libraries) for library in libraries: library_name = os.path.basename(library) self.assertIn(library_name, installed_libraries_list) - # Make sure all headers were installed + # Make sure all headers were installed. installed_headers = get_headers_list(install_prefix) missing_headers = list(set([os.path.basename(x) for x in headers]) - (set([os.path.basename(x) for x in installed_headers])) ) self.assertEqual(len(missing_headers), 0) - # Now create a test CMake project and try to find_package ns-3 + # Now create a test CMake project and try to find_package ns-3. test_main_file = os.sep.join([install_prefix, "main.cpp"]) with open(test_main_file, "w") as f: - f.write("# include \nint main() { return 0; }\n") + f.write(""" + #include + using namespace ns3; + int main () + { + Simulator::Stop (Seconds (1.0)); + Simulator::Run (); + Simulator::Destroy (); + return 0; + } + """) # We try to use this library without specifying a version, # specifying ns3-01 (text version with 'dev' is not supported) @@ -787,7 +1008,7 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): with open(test_cmake_project_file, "w") as f: f.write(test_cmake_project) - # Configure the test project and build + # Configure the test project cmake = shutil.which("cmake") return_code, stdout, stderr = run_program(cmake, "-DCMAKE_BUILD_TYPE=debug .", @@ -801,15 +1022,20 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): self.assertEqual(return_code, 0) self.assertIn("Build files", stdout) + # Build the test project making use of import ns-3 return_code, stdout, stderr = run_program("cmake", "--build .", cwd=install_prefix) if version == "3.00": self.assertEqual(return_code, 2) - self.assertIn("cannot", stderr) + self.assertGreater(len(stderr), 0) else: self.assertEqual(return_code, 0) self.assertIn("Built target", stdout) + # Try running the test program that imports ns-3 + return_code, stdout, stderr = run_program("./test", "", cwd=install_prefix) + self.assertEqual(return_code, 0) + # Uninstall return_code, stdout, stderr = run_ns3("uninstall") self.assertIn("Built target uninstall", stdout) @@ -823,31 +1049,45 @@ class NS3BuildBaseTestCase(NS3BaseTestCase): class NS3ExpectedUseTestCase(NS3BaseTestCase): + """! + Tests ns3 usage in more realistic scenarios + """ + + ## when cleaned_once is False, clean up build artifacts and reconfigure cleaned_once = False def setUp(self): - """ + """! Reuse cleaning/release configuration from NS3BaseTestCase if flag is cleaned - Here examples, tests and documentation are also enabled + Here examples, tests and documentation are also enabled. + @return None """ if not NS3ExpectedUseTestCase.cleaned_once: NS3ExpectedUseTestCase.cleaned_once = True NS3BaseTestCase.cleaned_once = False super().setUp() - # On top of the release build configured by NS3ConfigureTestCase, also enable examples, tests and docs - return_code, stdout, stderr = run_ns3("configure --enable-examples --enable-tests --enable-documentation") + # On top of the release build configured by NS3ConfigureTestCase, also enable examples, tests and docs. + return_code, stdout, stderr = run_ns3("configure --enable-examples --enable-tests") self.config_ok(return_code, stdout) - # Check if build-status.py exists, then read to get list of executables + # Check if build-status.py exists, then read to get list of executables. self.assertTrue(os.path.exists(usual_build_status_script)) + + ## ns3_executables holds a list of executables in build-status.py self.ns3_executables = get_programs_list() - # Check if c4che.py exists than read to get the list of enabled modules + # Check if c4che.py exists than read to get the list of enabled modules. self.assertTrue(os.path.exists(usual_c4che_script)) + + ## ns3_modules holds a list to the modules enabled stored in c4che.py self.ns3_modules = get_enabled_modules() def test_01_BuildProject(self): + """! + Try to build the project + @return None + """ return_code, stdout, stderr = run_ns3("build") self.assertEqual(return_code, 0) self.assertIn("Built target", stdout) @@ -859,61 +1099,261 @@ class NS3ExpectedUseTestCase(NS3BaseTestCase): self.assertIn(cmake_build_project_command, stdout) def test_02_BuildAndRunExistingExecutableTarget(self): - return_code, stdout, stderr = run_ns3('--run "test-runner --list"') + """! + Try to build and run test-runner + @return None + """ + return_code, stdout, stderr = run_ns3('run "test-runner --list"') self.assertEqual(return_code, 0) self.assertIn("Built target test-runner", stdout) self.assertIn(cmake_build_target_command(target="test-runner"), stdout) def test_03_BuildAndRunExistingLibraryTarget(self): - return_code, stdout, stderr = run_ns3("--run core") # this should not work + """! + Try to build and run a library + @return None + """ + return_code, stdout, stderr = run_ns3("run core") # this should not work self.assertEqual(return_code, 1) self.assertIn("Couldn't find the specified program: core", stderr) def test_04_BuildAndRunNonExistingTarget(self): - return_code, stdout, stderr = run_ns3("--run nonsense") # this should not work + """! + Try to build and run an unknown target + @return None + """ + return_code, stdout, stderr = run_ns3("run nonsense") # this should not work self.assertEqual(return_code, 1) self.assertIn("Couldn't find the specified program: nonsense", stderr) def test_05_RunNoBuildExistingExecutableTarget(self): - return_code, stdout, stderr = run_ns3('--run-no-build "test-runner --list"') + """! + Try to run test-runner without building + @return None + """ + return_code, stdout, stderr = run_ns3('run "test-runner --list" --no-build ') self.assertEqual(return_code, 0) self.assertNotIn("Built target test-runner", stdout) self.assertNotIn(cmake_build_target_command(target="test-runner"), stdout) def test_06_RunNoBuildExistingLibraryTarget(self): - return_code, stdout, stderr = run_ns3("--run-no-build core") # this should not work + """! + Test ns3 fails to run a library + @return None + """ + return_code, stdout, stderr = run_ns3("run core --no-build") # this should not work self.assertEqual(return_code, 1) self.assertIn("Couldn't find the specified program: core", stderr) def test_07_RunNoBuildNonExistingExecutableTarget(self): - return_code, stdout, stderr = run_ns3("--run-no-build nonsense") # this should not work + """! + Test ns3 fails to run an unknown program + @return None + """ + return_code, stdout, stderr = run_ns3("run nonsense --no-build") # this should not work self.assertEqual(return_code, 1) self.assertIn("Couldn't find the specified program: nonsense", stderr) def test_08_RunNoBuildGdb(self): - return_code, stdout, stderr = run_ns3("--run-no-build scratch-simulator --gdb") + """! + Test if scratch simulator is executed through gdb + @return None + """ + return_code, stdout, stderr = run_ns3("run scratch-simulator --gdb --no-build") self.assertEqual(return_code, 0) self.assertIn("scratch-simulator", stdout) self.assertIn("No debugging symbols found", stdout) def test_09_RunNoBuildValgrind(self): - return_code, stdout, stderr = run_ns3("--run-no-build scratch-simulator --valgrind") + """! + Test if scratch simulator is executed through valgrind + @return None + """ + return_code, stdout, stderr = run_ns3("run scratch-simulator --valgrind --no-build") self.assertEqual(return_code, 0) self.assertIn("scratch-simulator", stderr) self.assertIn("Memcheck", stderr) def test_10_DoxygenWithBuild(self): - return_code, stdout, stderr = run_ns3("--doxygen") + """! + Test the doxygen target that does trigger a full build + @return None + """ + doc_folder = os.path.abspath(os.sep.join([".", "doc"])) + + doxygen_files = ["introspected-command-line.h", "introspected-doxygen.h"] + for filename in doxygen_files: + file_path = os.sep.join([doc_folder, filename]) + if os.path.exists(file_path): + os.remove(file_path) + + # Rebuilding dot images is super slow, so not removing doxygen products + # doxygen_build_folder = os.sep.join([doc_folder, "html"]) + # if os.path.exists(doxygen_build_folder): + # shutil.rmtree(doxygen_build_folder) + + return_code, stdout, stderr = run_ns3("docs doxygen") self.assertEqual(return_code, 0) self.assertIn(cmake_build_target_command(target="doxygen"), stdout) self.assertIn("Built target doxygen", stdout) def test_11_DoxygenWithoutBuild(self): - return_code, stdout, stderr = run_ns3("--doxygen-no-build") + """! + Test the doxygen target that doesn't trigger a full build + @return None + """ + # Rebuilding dot images is super slow, so not removing doxygen products + # doc_folder = os.path.abspath(os.sep.join([".", "doc"])) + # doxygen_build_folder = os.sep.join([doc_folder, "html"]) + # if os.path.exists(doxygen_build_folder): + # shutil.rmtree(doxygen_build_folder) + + return_code, stdout, stderr = run_ns3("docs doxygen-no-build") self.assertEqual(return_code, 0) self.assertIn(cmake_build_target_command(target="doxygen-no-build"), stdout) self.assertIn("Built target doxygen-no-build", stdout) + def test_12_SphinxDocumentation(self): + """! + Test every individual target for Sphinx-based documentation + @return None + """ + doc_folder = os.path.abspath(os.sep.join([".", "doc"])) + + # First we need to clean old docs, or it will not make any sense. + for target in ["manual", "models", "tutorial"]: + doc_build_folder = os.sep.join([doc_folder, target, "build"]) + if os.path.exists(doc_build_folder): + shutil.rmtree(doc_build_folder) + + # For each sphinx doc target. + for target in ["manual", "models", "tutorial"]: + # Build + return_code, stdout, stderr = run_ns3("docs %s" % target) + self.assertEqual(return_code, 0) + self.assertIn(cmake_build_target_command(target="sphinx_%s" % target), stdout) + self.assertIn("Built target sphinx_%s" % target, stdout) + + # Check if the docs output folder exists + doc_build_folder = os.sep.join([doc_folder, target, "build"]) + self.assertTrue(os.path.exists(doc_build_folder)) + + # Check if the all the different types are in place (latex, split HTML and single page HTML) + for build_type in ["latex", "html", "singlehtml"]: + self.assertTrue(os.path.exists(os.sep.join([doc_build_folder, build_type]))) + + def test_13_Documentation(self): + """! + Test the documentation target that builds + both doxygen and sphinx based documentation + @return None + """ + doc_folder = os.path.abspath(os.sep.join([".", "doc"])) + + # First we need to clean old docs, or it will not make any sense. + + # Rebuilding dot images is super slow, so not removing doxygen products + # doxygen_build_folder = os.sep.join([doc_folder, "html"]) + # if os.path.exists(doxygen_build_folder): + # shutil.rmtree(doxygen_build_folder) + + for target in ["manual", "models", "tutorial"]: + doc_build_folder = os.sep.join([doc_folder, target, "build"]) + if os.path.exists(doc_build_folder): + shutil.rmtree(doc_build_folder) + + return_code, stdout, stderr = run_ns3("docs all") + self.assertEqual(return_code, 0) + self.assertIn(cmake_build_target_command(target="sphinx"), stdout) + self.assertIn("Built target sphinx", stdout) + self.assertIn(cmake_build_target_command(target="doxygen"), stdout) + self.assertIn("Built target doxygen", stdout) + + def test_14_Check(self): + """! + Test if ns3 --check is working as expected + @return None + """ + return_code, stdout, stderr = run_ns3("--check") + self.assertEqual(return_code, 0) + + def test_15_EnableSudo(self): + """! + Try to set ownership of scratch-simulator from current user to root, + and change execution permissions + @return None + """ + + # Test will be skipped if not defined + sudo_password = os.getenv("SUDO_PASSWORD", None) + + # Skip test if variable containing sudo password is the default value + if sudo_password is None: + return + + enable_sudo = read_c4che_entry("ENABLE_SUDO") + self.assertFalse(enable_sudo is True) + + # First we run to ensure the program was built + return_code, stdout, stderr = run_ns3('run scratch-simulator') + self.assertEqual(return_code, 0) + self.assertIn("Built target scratch_scratch-simulator", stdout) + self.assertIn(cmake_build_target_command(target="scratch_scratch-simulator"), stdout) + scratch_simulator_path = list(filter(lambda x: x if "scratch-simulator" in x else None, + self.ns3_executables + ) + )[-1] + prev_fstat = os.stat(scratch_simulator_path) # we get the permissions before enabling sudo + + # Now try setting the sudo bits from the run subparser + return_code, stdout, stderr = run_ns3('run scratch-simulator --enable-sudo', + env={"SUDO_PASSWORD": sudo_password}) + self.assertEqual(return_code, 0) + self.assertIn("Built target scratch_scratch-simulator", stdout) + self.assertIn(cmake_build_target_command(target="scratch_scratch-simulator"), stdout) + fstat = os.stat(scratch_simulator_path) + + import stat + # If we are on Windows, these permissions mean absolutely nothing, + # and on Fuse builds they might not make any sense, so we need to skip before failing + likely_fuse_mount = ((prev_fstat.st_mode & stat.S_ISUID) == (fstat.st_mode & stat.S_ISUID)) and \ + prev_fstat.st_uid == 0 + + if sys.platform == "win32" or likely_fuse_mount: + return + + # If this is a valid platform, we can continue + self.assertEqual(fstat.st_uid, 0) # check the file was correctly chown'ed by root + self.assertEqual(fstat.st_mode & stat.S_ISUID, stat.S_ISUID) # check if normal users can run as sudo + + # Now try setting the sudo bits as a post-build step (as set by configure subparser) + return_code, stdout, stderr = run_ns3('configure --enable-sudo') + self.assertEqual(return_code, 0) + + # Check if it was properly set in the c4che file + enable_sudo = read_c4che_entry("ENABLE_SUDO") + self.assertTrue(enable_sudo) + + # Remove old executables + for executable in self.ns3_executables: + if os.path.exists(executable): + os.remove(executable) + + # Try to build and then set sudo bits as a post-build step + return_code, stdout, stderr = run_ns3('build', env={"SUDO_PASSWORD": sudo_password}) + self.assertEqual(return_code, 0) + + # Check if commands are being printed for every target + self.assertIn("chown root", stdout) + self.assertIn("chmod u+s", stdout) + for executable in self.ns3_executables: + self.assertIn(os.path.basename(executable), stdout) + + # Check scratch simulator yet again + fstat = os.stat(scratch_simulator_path) + self.assertEqual(fstat.st_uid, 0) # check the file was correctly chown'ed by root + self.assertEqual(fstat.st_mode & stat.S_ISUID, stat.S_ISUID) # check if normal users can run as sudo + if __name__ == '__main__': loader = unittest.TestLoader()