open Import let runtest_info = let doc = "Run tests." in let man = [ `S "DESCRIPTION" ; `P "Run the given tests. The [TEST] argument can be either:" ; `I ( "-" , "A directory: If a directory is provided, dune will recursively run all tests \ within that directory." ) ; `I ( "-" , "A file name: If a specific file name is provided, dune will run the tests \ with that name." ) ; `P "If no [TEST] is provided, dune will run all tests in the current directory and \ its subdirectories." ; `P "See EXAMPLES below for additional information on use cases." ; `Blocks Common.help_secs ; Common.examples [ "Run all tests in a given directory", "dune runtest path/to/dir/" ; "Run a specific cram test", "dune runtest path/to/mytest.t" ; ( "Run all tests in the current source tree (including those that passed on \ the last run)" , "dune runtest --force" ) ; ( "Run tests sequentially without output buffering" , "dune runtest --no-buffer -j 1" ) ; "Run tests in a specific build context", "dune runtest _build/my_context/" ] ] in Cmd.info "runtest" ~doc ~man ~envs:Common.envs ;; let find_cram_test path ~parent_dir = let open Memo.O in Source_tree.nearest_dir parent_dir >>= Dune_rules.Cram_rules.cram_tests (* We ignore the errors we get when searching for cram tests as they will be reported during building anyway. We are only interested in the presence of cram tests. *) >>| List.filter_map ~f:Result.to_option (* We search our list of known cram tests for the test we are looking for. *) >>| List.find ~f:(fun (test : Source.Cram_test.t) -> let src = match test with | File src -> src | Dir { dir = src; _ } -> src in Path.Source.equal path src) ;; let explain_unsuccessful_search path ~parent_dir = let open Memo.O in (* If the user misspelled the test name, we give them a hint. *) let+ hints = (* We search for all files and directories in the parent directory and suggest them as possible candidates. *) let+ candidates = let+ file_candidates = let+ files = Source_tree.files_of parent_dir in Path.Source.Set.to_list_map files ~f:Path.Source.to_string and+ dir_candidates = let* parent_source_dir = Source_tree.find_dir parent_dir in match parent_source_dir with | None -> Memo.return [] | Some parent_source_dir -> let dirs = Source_tree.Dir.sub_dirs parent_source_dir in String.Map.to_list dirs |> Memo.List.map ~f:(fun (_candidate, candidate_path) -> Source_tree.Dir.sub_dir_as_t candidate_path >>| Source_tree.Dir.path >>| Path.Source.to_string) in List.concat [ file_candidates; dir_candidates ] in User_message.did_you_mean (Path.Source.to_string path) ~candidates in User_error.raise ~hints [ Pp.textf "%S does not match any known test." (Path.Source.to_string path) ] ;; (* [disambiguate_test_name path] is a function that takes in a directory [path] and classifies it as either a cram test or a directory to run tests in. *) let disambiguate_test_name path = match Path.Source.parent path with | None -> Memo.return @@ `Runtest (Path.source Path.Source.root) | Some parent_dir -> let open Memo.O in find_cram_test path ~parent_dir >>= (function | Some test -> (* If we find the cram test, then we request that is run. *) Memo.return (`Test (parent_dir, Source.Cram_test.name test)) | None -> (* If we don't find it, then we assume the user intended a directory for @runtest to be used. *) Source_tree.find_dir path >>= (function (* We need to make sure that this directory or file exists. *) | Some _ -> Memo.return (`Runtest (Path.source path)) | None -> explain_unsuccessful_search path ~parent_dir)) ;; let runtest_term = let name = Arg.info [] ~docv:"TEST" in let+ builder = Common.Builder.term and+ dirs = Arg.(value & pos_all string [ "." ] name) in let common, config = Common.init builder in let request (setup : Import.Main.build_system) = let contexts = setup.contexts in List.map dirs ~f:(fun dir -> let dir = Path.of_string dir |> Path.Expert.try_localize_external in let open Action_builder.O in let* contexts, alias_kind = match (Util.check_path contexts dir : Util.checked) with | In_build_dir (context, dir) -> let+ res = Action_builder.of_memo (disambiguate_test_name dir) in [ context ], res | In_source_dir dir -> (* We need to adjust the path here to make up for the current working directory. *) let { Workspace_root.to_cwd; _ } = Common.root common in let dir = Path.Source.L.relative Path.Source.root (to_cwd @ Path.Source.explode dir) in let+ res = Action_builder.of_memo (disambiguate_test_name dir) in contexts, res | In_private_context _ | In_install_dir _ -> User_error.raise [ Pp.textf "This path is internal to dune: %s" (Path.to_string_maybe_quoted dir) ] | External _ -> User_error.raise [ Pp.textf "This path is outside the workspace: %s" (Path.to_string_maybe_quoted dir) ] in Alias.request @@ match alias_kind with | `Test (dir, alias_name) -> Alias.in_dir ~name:(Dune_engine.Alias.Name.of_string alias_name) ~recursive:false ~contexts (Path.source dir) | `Runtest dir -> Alias.in_dir ~name:Dune_rules.Alias.runtest ~recursive:true ~contexts dir) |> Action_builder.all_unit in Build.run_build_command ~common ~config ~request ;; let commands = let command = Cmd.v runtest_info runtest_term in [ command; command_alias command runtest_term "test" ] ;;