note description: "[ Gobo Eiffel Cop. An Eiffel language conformance validation suite. ]" copyright: "Copyright (c) 2018-2021, Eric Bezault and others" license: "MIT License" date: "$Date$" revision: "$Revision$" class GECOP inherit GECOP_VERSION KL_SHARED_EXCEPTIONS KL_SHARED_ARGUMENTS KL_SHARED_FILE_SYSTEM KL_SHARED_EXECUTION_ENVIRONMENT KL_SHARED_OPERATING_SYSTEM KL_SHARED_STANDARD_FILES KL_IMPORTED_STRING_ROUTINES create execute, execute_with_arguments, execute_with_arguments_and_error_handler feature -- Execution execute -- Start 'gecop' execution, reading arguments from the command-line. do execute_with_arguments (Arguments.to_array) Exceptions.die (exit_code) rescue Exceptions.die (4) end execute_with_arguments (a_args: ARRAY [STRING]) -- Start 'gecop' execution with arguments `a_args'. -- Set `exit_code'. require a_args_not_void: a_args /= Void no_void_arg: across a_args as i_arg all i_arg /= Void end local l_error_handler: UT_ERROR_HANDLER do create l_error_handler.make_standard execute_with_arguments_and_error_handler (a_args, l_error_handler) end execute_with_arguments_and_error_handler (a_args: ARRAY [STRING]; a_error_handler: UT_ERROR_HANDLER) -- Start 'gecop' execution. -- Set `exit_code'. require a_args_not_void: a_args /= Void no_void_arg: across a_args as i_arg all i_arg /= Void end a_error_handler_not_void: a_error_handler /= Void local l_validation_directory_name: STRING l_directory: KL_DIRECTORY l_relative_path: DS_ARRAYED_LIST [STRING] l_tested_eiffel_tool: STRING l_tester: TS_TESTER l_test_suite: TS_TEST_SUITE l_output_stream: KL_STRING_OUTPUT_STREAM l_output_string: STRING l_aggregate, l_diff: BOOLEAN l_set_up_mutex: MUTEX do Arguments.set_program_name ("gecop") create error_handler.make_standard parse_arguments (a_args) if exit_code = 0 and then not version_flag.was_found then if tool_option.was_found and then attached tool_option.parameter as l_tool_name then l_tested_eiffel_tool := l_tool_name else l_tested_eiffel_tool := default_tested_eiffel_tool end if validation_option.was_found and then attached validation_option.parameter as l_directory_name and then not l_directory_name.is_empty then l_validation_directory_name := l_directory_name else l_validation_directory_name := default_validation_dirname end l_validation_directory_name := Execution_environment.interpreted_string (l_validation_directory_name) create l_directory.make (l_validation_directory_name) l_directory.open_read if l_directory.is_open_read then create l_tester.make_default if tool_executable_option.was_found and then attached tool_executable_option.parameter as l_executable_filename and then not l_executable_filename.is_empty then l_tester.variables.set_value ("executable", l_executable_filename) end create l_test_suite.make ("validation", l_tester.variables) l_tester.set_suite (l_test_suite) create l_relative_path.make_default create l_set_up_mutex.make process_directory (l_directory, l_relative_path, l_tested_eiffel_tool, filter, l_tester, l_set_up_mutex) l_directory.close create l_output_stream.make_empty l_aggregate := not aggregate_option.was_found or else aggregate_option.parameter run_tests (l_tester, l_aggregate, l_output_stream) l_output_string := l_output_stream.string l_output_string := text_to_markdown (l_output_string) l_diff := not diff_option.was_found or else diff_option.parameter if l_diff then report_diff_with_last_run (l_output_string, l_validation_directory_name, l_tested_eiffel_tool) end if keep_testdir_flag.was_found then write_last_run_file (l_output_string, l_tested_eiffel_tool) else file_system.recursive_delete_directory (test_dirname) end else report_cannot_read_directory_error (l_validation_directory_name) exit_code := 1 end end rescue exit_code := 4 end feature {NONE} -- Processing process_directory (a_directory: KL_DIRECTORY; a_relative_path: DS_ARRAYED_LIST [STRING]; a_tested_eiffel_tool: STRING; a_filter: detachable RX_REGULAR_EXPRESSION; a_tester: TS_TESTER; a_set_up_mutex: detachable MUTEX) -- Traverse `a_directory' and recursively its subdirectories to find -- test cases to be added to `a_tester' (if matching `a_filter') and -- later run with `a_tested_eiffel_tool'. require a_directory_not_void: a_directory /= Void a_directory_open_read: a_directory.is_open_read a_relative_path_not_void: a_relative_path /= Void no_void_relative_path: not a_relative_path.has_void a_tested_eiffel_tool_not_void: a_tested_eiffel_tool /= Void a_filter_is_compiled: a_filter /= Void implies a_filter.is_compiled a_tester_not_void: a_tester /= Void local l_dirname: STRING l_entries: DS_ARRAYED_LIST [STRING] l_entry_name: STRING l_entry_fullname: STRING i, nb: INTEGER l_subdirectory: KL_DIRECTORY l_test_name: STRING l_test_case: EIFFEL_TEST_CASE l_test_suite: detachable TS_TEST_SUITE l_test_suite_name: STRING l_root_name: STRING l_comparator: UC_STRING_COMPARATOR l_sorter: DS_QUICK_SORTER [STRING] l_test_dirname: STRING do l_dirname := a_directory.name create l_entries.make_default from a_directory.read_entry until a_directory.end_of_input loop l_entry_name := a_directory.last_entry if l_entry_name.count > 0 and not STRING_.same_string (l_entry_name, file_system.relative_current_directory) and not STRING_.same_string (l_entry_name, file_system.relative_parent_directory) then l_entries.force_last (l_entry_name.twin) end a_directory.read_entry end create l_comparator create l_sorter.make (l_comparator) l_entries.sort (l_sorter) nb := l_entries.count from i := 1 until i > nb loop l_entry_name := l_entries.item (i) l_entry_fullname := file_system.pathname (l_dirname, l_entry_name) create l_subdirectory.make (l_entry_fullname) l_subdirectory.open_read if l_subdirectory.is_open_read then if a_relative_path.is_empty then l_test_name := l_entry_name l_test_suite_name := "validation" else if a_relative_path.count = 1 then l_test_suite_name := a_relative_path.first else l_root_name := a_relative_path.first a_relative_path.remove_first l_test_suite_name := unix_file_system.nested_pathname (l_root_name, a_relative_path.to_array) a_relative_path.force_first (l_root_name) end l_test_name := unix_file_system.pathname (l_test_suite_name, l_entry_name) end a_relative_path.force_last (l_entry_name) if l_entry_name.count >= 4 and then l_entry_name.starts_with ("test") then if a_filter = Void or else a_filter.matches (l_test_name) then if l_test_suite = Void then create l_test_suite.make (l_test_suite_name, a_tester.variables) a_tester.suite.put_test (l_test_suite) end l_test_dirname := file_system.nested_pathname (test_dirname, a_relative_path.to_array) create l_test_case.make (l_entry_fullname, l_test_dirname) l_test_case.set_test (l_test_name, agent l_test_case.compile_and_test (a_tested_eiffel_tool)) l_test_case.set_variables (a_tester.variables) l_test_case.set_set_up_mutex (a_set_up_mutex) l_test_suite.put_test (l_test_case) end else process_directory (l_subdirectory, a_relative_path, a_tested_eiffel_tool, a_filter, a_tester, a_set_up_mutex) end a_relative_path.remove_last l_subdirectory.close end i := i + 1 end end run_tests (a_tester: TS_TESTER; a_aggregate: BOOLEAN; a_output_file: KI_TEXT_OUTPUT_STREAM) -- Run tests in `a_tester'. -- Aggregate the test results if `a_aggregate' is true. -- Write the test results to the console window and to `a_output_file'. require a_tester_not_void: a_tester /= Void a_output_file_not_void: a_output_file /= Void a_output_file_open_write: a_output_file.is_open_write local l_summary: TS_SUMMARY l_tester: TS_TESTER l_test_suite: TS_TEST_SUITE l_has_test_suite: BOOLEAN l_has_test_case: BOOLEAN l_thread_count: INTEGER l_old_test_suite: TS_TEST_SUITE do if a_aggregate then std.output.put_new_line std.output.put_line ("Testing " + a_tester.suite.name + "...") std.output.put_line ("Running Test Cases") std.output.put_new_line create l_summary.make l_summary.set_sort_errors (True) l_thread_count := thread_count if l_thread_count > 1 then l_old_test_suite := a_tester.suite l_test_suite := {TS_TEST_SUITE_FACTORY}.new_test_suite (l_old_test_suite.name, l_old_test_suite.variables, l_thread_count) l_old_test_suite.add_test_cases_to_suite (l_test_suite) a_tester.set_suite (l_test_suite) a_tester.execute_with_summary (l_summary, std.output) a_tester.set_suite (l_old_test_suite) else a_tester.execute_with_summary (l_summary, std.output) end -- Write to `a_output_file'. a_output_file.put_new_line a_output_file.put_line ("Testing " + a_tester.suite.name + "...") a_output_file.put_line ("Running Test Cases") a_output_file.put_new_line l_summary.print_summary_without_assertions (a_tester.suite, a_output_file) if not l_summary.is_successful then a_output_file.put_new_line l_summary.print_errors (a_output_file) end else across a_tester.suite as i_tests loop if attached {TS_TEST_SUITE} i_tests as l_suite then create l_tester.make_default l_tester.set_suite (l_suite) run_tests (l_tester, False, a_output_file) l_has_test_suite := True else l_has_test_case := True end end if not l_has_test_suite then run_tests (a_tester, True, a_output_file) elseif l_has_test_case then create l_test_suite.make (a_tester.suite.name, a_tester.suite.variables) across a_tester.suite as i_tests loop if not attached {TS_TEST_SUITE} i_tests then l_test_suite.put_test (i_tests) end end create l_tester.make_default l_tester.set_suite (l_test_suite) run_tests (l_tester, True, a_output_file) end end end write_last_run_file (a_output_string: STRING; a_tested_eiffel_tool: STRING) -- Write `a_output_string' to last run file associated with `a_tested_eiffel_tool'. require a_output_string_not_void: a_output_string /= Void a_tested_eiffel_tool_not_void: a_tested_eiffel_tool /= Void local l_output_filename: STRING l_output_file: KL_TEXT_OUTPUT_FILE do if attached last_run_filename_suffix (a_tested_eiffel_tool) as l_last_run_filename_suffix then l_output_filename := file_system.pathname (test_dirname, "last_run" + l_last_run_filename_suffix + ".md") else l_output_filename := file_system.pathname (test_dirname, "last_run.log") end create l_output_file.make (l_output_filename) l_output_file.recursive_open_write if l_output_file.is_open_write then l_output_file.put_string (a_output_string) l_output_file.close else report_cannot_write_to_file_error (l_output_filename) exit_code := 1 end end report_diff_with_last_run (a_output_string: STRING; a_validation_directory_name: STRING; a_tested_eiffel_tool: STRING) -- Report whether diffs were found in the test results `a_output_string' -- since last run for `a_tested_eiffel_tool'. require a_output_string_not_void: a_output_string /= Void a_validation_directory_name_not_void: a_validation_directory_name /= Void a_tested_eiffel_tool_not_void: a_tested_eiffel_tool /= Void local l_input_filename: STRING l_input_file: KL_TEXT_INPUT_FILE l_input_string: STRING l_has_diff: BOOLEAN do if attached last_run_filename_suffix (a_tested_eiffel_tool) as l_last_run_filename_suffix then l_input_filename := file_system.pathname (a_validation_directory_name, "last_run" + l_last_run_filename_suffix + ".md") create l_input_file.make (l_input_filename) l_input_file.open_read if l_input_file.is_open_read then create l_input_string.make (a_output_string.count) from l_input_file.read_string (4096) until l_input_file.end_of_file loop l_input_string.append_string (l_input_file.last_string) l_input_file.read_string (4096) end l_input_file.close l_has_diff := not l_input_string.same_string (a_output_string) else l_has_diff := True end else l_has_diff := True end std.output.put_new_line if l_has_diff then std.output.put_line ("DIFFS FOUND SINCE LAST RUN") else std.output.put_line ("No Diff since last run") end std.output.put_new_line end text_to_markdown (a_string: STRING): STRING -- Convert `a_string' to a Markdown hyperlinked text. -- This will allow to jump directly to the failing test case folder -- when browsing in a Markdown aware environment (like GitHub). require a_string_not_void: a_string /= Void local l_test_case_name_rexgexp: RX_PCRE_REGULAR_EXPRESSION do create l_test_case_name_rexgexp.make l_test_case_name_rexgexp.compile ("\[([^\]]+)\]") l_test_case_name_rexgexp.match (a_string) Result := l_test_case_name_rexgexp.replace_all ("\[[\1\](\1\)\]") Result.replace_substring_all ("_", "\_") Result.replace_substring_all ("...", "...</br>") Result.replace_substring_all ("#", " #") Result.replace_substring_all ("%NFAIL", "</br>%NFAIL") end feature -- Error handling error_handler: UT_ERROR_HANDLER -- Error handler report_cannot_read_directory_error (a_dirname: STRING) -- Report that `a_dirname' cannot be -- opened in read mode. require a_dirname_not_void: a_dirname /= Void local an_error: UT_CANNOT_READ_DIRECTORY_ERROR do create an_error.make (a_dirname) error_handler.report_error (an_error) end report_cannot_write_to_file_error (a_filename: STRING) -- Report that `a_filename' cannot be -- opened in write mode. require a_filename_not_void: a_filename /= Void local an_error: UT_CANNOT_WRITE_TO_FILE_ERROR do create an_error.make (a_filename) error_handler.report_error (an_error) end report_invalid_filter_regexp (a_filter_regexp: STRING) -- Report that `a_filter_pattern' is not a -- valid regular expression. require a_filter_regexp_not_void: a_filter_regexp /= Void local l_error: UT_INVALID_REGULAR_EXPRESSION_ERROR do create l_error.make (a_filter_regexp) error_handler.report_error (l_error) end report_version_number -- Report version number. local a_message: UT_VERSION_NUMBER do create a_message.make (Version_number) error_handler.report_info (a_message) end exit_code: INTEGER -- Exit code feature -- Access thread_count: INTEGER -- Number of threads to be used do Result := {EXECUTION_ENVIRONMENT}.available_cpu_count.as_integer_32 if thread_option.was_found then Result := thread_option.parameter if Result <= 0 then Result := {EXECUTION_ENVIRONMENT}.available_cpu_count.as_integer_32 + Result end end if Result < 1 or not {PLATFORM}.is_thread_capable then Result := 1 end ensure thread_count_not_negative: Result >= 1 end filter: detachable RX_PCRE_REGULAR_EXPRESSION -- When specified, run only the test cases matching this regexp feature -- Argument parsing tool_option: AP_ENUMERATION_OPTION -- Option for '--tool=<eiffel_tool>' tool_executable_option: AP_STRING_OPTION -- Option for '--tool_executable=<executable_filename>' validation_option: AP_STRING_OPTION -- Option for '--validation=<directory_name>' filter_option: AP_STRING_OPTION -- Option for '--filter=<regexp>' aggregate_option: AP_BOOLEAN_OPTION -- Option for '--aggregate=<no|yes>' diff_option: AP_BOOLEAN_OPTION -- Option for '--diff=<no|yes>' keep_testdir_flag: AP_FLAG -- Flag for '--keep-testdir' thread_option: AP_INTEGER_OPTION -- Option for '--thread=<thread_count>' version_flag: AP_FLAG -- Flag for '--version' parse_arguments (a_args: ARRAY [STRING]) -- Initialize options and parse arguments `a_args'. require a_args_not_void: a_args /= Void no_void_arg: across a_args as i_arg all i_arg /= Void end local l_parser: AP_PARSER l_list: AP_ALTERNATIVE_OPTIONS_LIST l_filter: like filter do create l_parser.make l_parser.set_application_description ("Gobo Eiffel Cop, an Eiffel language conformance validation suite.") l_parser.set_parameters_description ("") -- tool create tool_option.make_with_long_form ("tool") tool_option.set_description ("Eiffel tool to be tested against the validation suite. (default: ge)") tool_option.extend ("ge") tool_option.extend ("ise") tool_option.extend ("gec") tool_option.extend ("gelint") tool_option.extend ("ge_debug") tool_option.extend ("ge_lint") tool_option.extend ("ise_debug") tool_option.extend ("ise_dotnet") tool_option.extend ("ise_dotnet_debug") l_parser.options.force_last (tool_option) -- tool_executable create tool_executable_option.make_with_long_form ("tool-executable") tool_executable_option.set_description ("Executable filename (optionally with a pathname) of Eiffel tool to be tested. (default: gec|gelint|ec in the PATH)") tool_executable_option.set_parameter_description ("filename") l_parser.options.force_last (tool_executable_option) -- validation create validation_option.make_with_long_form ("validation") validation_option.set_description ("Directory containing the Eiffel validation suite. (default: $GOBO/tool/gecop/validation)") validation_option.set_parameter_description ("directory_name") l_parser.options.force_last (validation_option) -- filter create filter_option.make_with_long_form ("filter") filter_option.set_description ("When specified, run only the test cases matching this regexp. (default: no filter)") filter_option.set_parameter_description ("regexp") l_parser.options.force_last (filter_option) -- aggregate create aggregate_option.make_with_long_form ("aggregate") aggregate_option.set_description ("Should test results be aggregated into a single report? (default: yes)") aggregate_option.set_parameter_description ("no|yes") l_parser.options.force_last (aggregate_option) -- diff create diff_option.make_with_long_form ("diff") diff_option.set_description ("Should test results be compared with the previous run? (default: yes)") diff_option.set_parameter_description ("no|yes") l_parser.options.force_last (diff_option) -- keep-testdir create keep_testdir_flag.make_with_long_form ("keep-testdir") keep_testdir_flag.set_description ("Do no delete temporary directory after running the validation suite. (default: delete testdir)") l_parser.options.force_last (keep_testdir_flag) -- thread create thread_option.make_with_long_form ("thread") thread_option.set_description ("Number of threads to be used. Negative numbers -N mean %"number of CPUs - N%". (default: number of CPUs)") thread_option.set_parameter_description ("thread_count") if {PLATFORM}.is_thread_capable then l_parser.options.force_last (thread_option) end -- version create version_flag.make ('V', "version") version_flag.set_description ("Print the version number of gecop and exit.") create l_list.make (version_flag) l_parser.alternative_options_lists.force_last (l_list) -- Parsing. l_parser.parse_array (a_args) if version_flag.was_found then report_version_number exit_code := 0 elseif not l_parser.parameters.is_empty then error_handler.report_info_message (l_parser.help_option.full_usage_instruction (l_parser)) exit_code := 1 elseif filter_option.was_found and then attached filter_option.parameter as l_filter_pattern and then not l_filter_pattern.is_empty then create l_filter.make if Operating_system.is_windows then l_filter.set_case_insensitive (True) end l_filter.compile (l_filter_pattern) if not l_filter.is_compiled then report_invalid_filter_regexp (l_filter_pattern) exit_code := 1 else filter := l_filter end end ensure tool_option_not_void: tool_option /= Void tool_executable_option_not_void: tool_executable_option /= Void validation_option_not_void: validation_option /= Void filter_option_not_void: filter_option /= Void aggregate_option_not_void: aggregate_option /= Void diff_option_not_void: diff_option /= Void keep_testdir_flag_not_void: keep_testdir_flag /= Void thread_option_not_void: thread_option /= Void version_flag_not_void: version_flag /= Void end feature {NONE} -- Implementation last_run_filename_suffix (a_tested_eiffel_tool: STRING): detachable STRING -- Filename suffix for the 'last_run' file associated with `a_tested_eiffel_tool' do if a_tested_eiffel_tool.starts_with ("ise") then Result := "_ise" elseif a_tested_eiffel_tool.starts_with ("ge") then if a_tested_eiffel_tool.ends_with ("lint") then Result := "_gelint" else Result := "_gec" end end end feature {NONE} -- Constants default_validation_dirname: STRING -- Default validation directory name once Result := file_system.nested_pathname ("$GOBO", <<"tool", "gecop", "validation">>) ensure default_validation_dirname_not_void: Result /= Void end default_tested_eiffel_tool: STRING = "ge" -- Name of Eiffel tool to be tested by default test_dirname: STRING = "Tvalidation" -- Name of temporary directory where to run the test invariant error_handler_not_void: error_handler /= Void tool_option_not_void: tool_option /= Void tool_executable_option_not_void: tool_executable_option /= Void validation_option_not_void: validation_option /= Void filter_option_not_void: filter_option /= Void aggregate_option_not_void: aggregate_option /= Void diff_option_not_void: diff_option /= Void keep_testdir_flag_not_void: keep_testdir_flag /= Void thread_option_not_void: thread_option /= Void version_flag_not_void: version_flag /= Void end