diff --git a/src/alr/alr-commands-test2.adb b/src/alr/alr-commands-test2.adb new file mode 100644 index 000000000..51375fb91 --- /dev/null +++ b/src/alr/alr-commands-test2.adb @@ -0,0 +1,57 @@ +with Alire.Directories; +with Alr.Test_Runner; + +package body Alr.Commands.Test2 is + overriding procedure Execute + (Cmd : in out Command; Args : AAA.Strings.Vector) + is + use Alire.Directories; + use GNAT.Strings; + + Subfolder : constant String := + (if Cmd.Directory.all /= "" then Cmd.Directory.all else "tests"); + begin + Cmd.Requires_Workspace; + Cmd.Set (Alire.Roots.Load_Root (Cmd.Root.Path / Subfolder)); + Cmd.Requires_Workspace (Sync => True); + + declare + G : Guard (Enter_Folder (Cmd.Root.Path)); + pragma Unreferenced (G); + begin + Alr.Test_Runner.Run + (Cmd.Root, AAA.Strings.Empty_Vector, Integer'Max (Cmd.Jobs, 0)); + end; + end Execute; + + ---------------------- + -- Long_Description -- + ---------------------- + + overriding function Long_Description + (Cmd : Command) return AAA.Strings.Vector is + (AAA.Strings.Empty_Vector.Append + ("Run tests in a predefined format using the (experimental) " & + "default alire test runner")); + + -------------------- + -- Setup_Switches -- + -------------------- + + overriding procedure Setup_Switches + (Cmd : in out Command; + Config : in out CLIC.Subcommand.Switches_Configuration) + is + use CLIC.Subcommand; + begin + Define_Switch + (Config, Cmd.Jobs'Access, "-j:", "--jobs=", + "Run up to N tests in parallel, or as many as processors " & + "if 0 (default)", Default => 0, Argument => "N"); + + Define_Switch + (Config, Cmd.Directory'Access, Long_Switch => "--dir=", + Help => "Run tests from the given folder (default: tests)", + Argument => ""); + end Setup_Switches; +end Alr.Commands.Test2; diff --git a/src/alr/alr-commands-test2.ads b/src/alr/alr-commands-test2.ads new file mode 100644 index 000000000..b0e096c55 --- /dev/null +++ b/src/alr/alr-commands-test2.ads @@ -0,0 +1,35 @@ +with AAA.Strings; +with GNAT.Strings; + +package Alr.Commands.Test2 is + + type Command is new Commands.Command with private; + + overriding function Name + (Cmd : Command) return CLIC.Subcommand.Identifier is + ("test2"); + + overriding procedure Execute + (Cmd : in out Command; Args : AAA.Strings.Vector); + + overriding function Long_Description + (Cmd : Command) return AAA.Strings.Vector; + + overriding procedure Setup_Switches + (Cmd : in out Command; + Config : in out CLIC.Subcommand.Switches_Configuration); + + overriding function Short_Description (Cmd : Command) return String is + ("Test the current crate with the default alire test runner"); + + overriding function Usage_Custom_Parameters (Cmd : Command) return String is + (""); + +private + + type Command is new Commands.Command with record + Jobs : aliased Integer; + Directory : aliased GNAT.Strings.String_Access; + end record; + +end Alr.Commands.Test2; diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index acaf016a1..310eec86c 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -43,6 +43,7 @@ with Alr.Commands.Search; with Alr.Commands.Settings; with Alr.Commands.Show; with Alr.Commands.Test; +with Alr.Commands.Test2; with Alr.Commands.Toolchain; with Alr.Commands.Update; with Alr.Commands.Version; @@ -770,6 +771,7 @@ begin Sub_Cmd.Register ("Testing", new Action.Command); Sub_Cmd.Register ("Testing", new Dev.Command); Sub_Cmd.Register ("Testing", new Test.Command); + Sub_Cmd.Register ("Testing", new Test2.Command); -- Help topics -- Sub_Cmd.Register (new Topics.Aliases.Topic); diff --git a/src/alr/alr-test_runner.adb b/src/alr/alr-test_runner.adb new file mode 100644 index 000000000..ae17cf47b --- /dev/null +++ b/src/alr/alr-test_runner.adb @@ -0,0 +1,213 @@ +with Ada.Command_Line; +with Ada.Containers.Indefinite_Ordered_Maps; +with Ada.Text_IO; +with GNAT.OS_Lib; +with System.Multiprocessors; + +with Alire; +with Alire.Directories; use Alire.Directories; +with Alire.OS_Lib; +with Alire.Utils.Text_Files; use Alire.Utils; + +with CLIC.TTY; + +package body Alr.Test_Runner is + + protected Driver is + -- Protected driver for synchronising stats and output + + procedure Pass (Msg : String); + -- Report a passing test with a message + + procedure Fail (Msg : String; Output : AAA.Strings.Vector); + -- Report a failing test with a message and its output + + function Total_Count return Natural; + -- Get the total number of tests that have been run + + function Fail_Count return Natural; + -- Get the number of failed tests + private + Passed : Natural := 0; + Failed : Natural := 0; + end Driver; + + protected body Driver is + procedure Pass (Msg : String) is + begin + Passed := Passed + 1; + Alr.Trace.Always ("[ " & CLIC.TTY.OK ("PASS") & " ] " & Msg); + end Pass; + + procedure Fail (Msg : String; Output : AAA.Strings.Vector) is + begin + Failed := Failed + 1; + Alr.Trace.Always ("[ " & CLIC.TTY.Error ("FAIL") & " ] " & Msg); + if not Output.Is_Empty then + Alr.Trace.Info ("*** Test output ***"); + for L of Output loop + Alr.Trace.Info (CLIC.TTY.Dim (L)); + end loop; + Alr.Trace.Info ("*** End Test output ***"); + end if; + end Fail; + + function Total_Count return Natural is (Passed + Failed); + function Fail_Count return Natural is (Failed); + end Driver; + + procedure Create_Gpr_List + (Root : Alire.Roots.Root; List : AAA.Strings.Vector) + -- Create a gpr file containing a list of the test files + -- (named `Test_Files`). + + is + File_Path : constant Alire.Absolute_Path := + Root.Path / "config" / (Root.Name.As_String & "_list_config.gpr"); + File : Text_Files.File := Text_Files.Create (File_Path); + Lines : access AAA.Strings.Vector renames File.Lines; + First : Boolean := True; + + Indent : constant String := " "; + + Root_Name : constant String := + AAA.Strings.To_Mixed_Case (Root.Name.As_String); + begin + Touch (File_Path, True); + + Lines.Append_Line ("abstract project " & Root_Name & "_List_Config is"); + Lines.Append_Line (Indent & "Test_Files := ("); + + for Name of List loop + Lines.Append_Line (Indent & Indent); + if First then + Lines.Append_To_Last_Line (" "); + First := False; + else + Lines.Append_To_Last_Line (","); + end if; + Lines.Append_To_Last_Line ("""" & Name & ".adb"""); + end loop; + + Lines.Append_Line (Indent & ");"); + Lines.Append_Line ("end " & Root_Name & "_List_Config;"); + end Create_Gpr_List; + + procedure Run_All_Tests + (Root : Alire.Roots.Root; Test_List : AAA.Strings.Vector; Jobs : Positive) + is + use GNAT.OS_Lib; + + function Cmp (A, B : Process_Id) return Boolean is + (Pid_To_Integer (A) < Pid_To_Integer (B)); + + package Map is new Ada.Containers.Indefinite_Ordered_Maps + (Process_Id, String, "<" => Cmp); + + Running_Tests : Map.Map := Map.Empty_Map; + Output_Files : Map.Map := Map.Empty_Map; + + procedure Spawn_Test (Test_Name : String) is + Exe_Name : constant String := Test_Name & Alire.OS_Lib.Exe_Suffix; + Filename : constant String := "output_" & Test_Name & ".tmp"; + + Args : constant Argument_List := (1 .. 0 => <>); + Pid : Process_Id; + begin + Pid := Non_Blocking_Spawn (Root.Path / "bin" / Exe_Name, + Args, Filename, Err_To_Out => True); + if Pid = Invalid_Pid then + Driver.Fail (Test_Name & " (failed to start!)", + AAA.Strings.Empty_Vector); + else + Running_Tests.Insert (Pid, Test_Name); + Output_Files.Insert (Pid, Filename); + end if; + end Spawn_Test; + + Pid : Process_Id; + Success : Boolean; + + Remaining : AAA.Strings.Vector := Test_List; + + begin + + -- start the first `Jobs` tests + for I in 1 .. Natural'Min (Jobs, Natural (Test_List.Length)) loop + Spawn_Test (Remaining.First_Element); + Remaining := Remaining.Tail; + end loop; + + loop + -- wait for one test to finish + Wait_Process (Pid, Success); + + if Pid = Invalid_Pid then + -- if no process was running, end the loop + exit; + end if; + + if Success then + Driver.Pass (Running_Tests (Pid)); + else + declare + use Alire.Utils.Text_Files; + Output : File := Load (Output_Files (Pid), False); + begin + Driver.Fail (Running_Tests (Pid), Output.Lines.all); + end; + end if; + + Delete_File (Output_Files (Pid), Success); + Running_Tests.Delete (Pid); + Output_Files.Delete (Pid); + + if not Remaining.Is_Empty then + -- start up a new test + Spawn_Test (Remaining.First_Element); + Remaining := Remaining.Tail; + end if; + end loop; + end Run_All_Tests; + + procedure Run + (Root : in out Alire.Roots.Root; + Args : AAA.Strings.Vector := AAA.Strings.Empty_Vector; + Jobs : Natural := 0) + is + Job_Count : constant Positive := + (if Jobs = 0 then Positive (System.Multiprocessors.Number_Of_CPUs) + else Jobs); + Path : constant Alire.Absolute_Path := Root.Path; + Test_List : AAA.Strings.Vector; + + procedure Append (Dir_Entry : Adirs.Directory_Entry_Type) is + -- Helper function to append all .adb files in a folder + -- to the `Test_List` vector + + Name : constant String := Adirs.Simple_Name (Dir_Entry); + begin + if Name'Length > 4 and then Name (Name'Last - 3 .. Name'Last) = ".adb" + then + Test_List.Append (Name (Name'First .. Name'Last - 4)); + end if; + end Append; + + begin + Adirs.Search (Path / "src", "", Process => Append'Access); + Create_Gpr_List (Root, Test_List); + + Alr.Trace.Info ("Building tests"); + if Alire.Roots.Build (Root, Args, Build_All_Deps => True) then + Alr.Trace.Info ("Running" & Test_List.Length'Image & " tests"); + Run_All_Tests (Root, Test_List, Job_Count); + + Alr.Trace.Info ("Total:" & Driver.Total_Count'Image & " tests"); + Ada.Text_IO.Flush; + if Driver.Fail_Count /= 0 then + Alr.Trace.Error ("failed" & Driver.Fail_Count'Image & " test"); + Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); + end if; + end if; + end Run; +end Alr.Test_Runner; diff --git a/src/alr/alr-test_runner.ads b/src/alr/alr-test_runner.ads new file mode 100644 index 000000000..786b8fdc7 --- /dev/null +++ b/src/alr/alr-test_runner.ads @@ -0,0 +1,12 @@ +with Alire.Roots; + +with AAA.Strings; + +package Alr.Test_Runner is + procedure Run + (Root : in out Alire.Roots.Root; + Args : AAA.Strings.Vector := AAA.Strings.Empty_Vector; + Jobs : Natural := 0); + -- Run all .adb files in the `src` folder of the given root as + -- separate tests. +end Alr.Test_Runner;