Testing & Debugging: AUnit, Assertions, Tracebacks & GDB

Write unit tests (a minimal harness and the AUnit framework structure), use contracts and assertions as test oracles, report failures through exit status for CI, get symbolic tracebacks (-g, binder -Es, GNAT.Traceback.Symbolic), and debug with GDB (catch exception, breakpoints, backtraces, printing Ada values), plus coverage and static analysis.

Ada's compiler and contracts catch many bugs early, but tests and debuggers are still essential. Contracts make tests stronger: every test run also checks all preconditions, postconditions and invariants on the way. When something fails, GNAT can print a symbolic traceback, and GDB understands Ada syntax, types and exceptions.

Unit Testing

AUnit (available through Alire) is the xUnit framework for Ada: test cases derive from AUnit.Test_Cases.Test_Case (or Simple_Test_Cases), register routines, and use Assert; suites group tests, and a harness runs them and reports results. The core ideas are small enough to build yourself: a generic Assert_Equal parameterized by the type and an Image function, an Assert_Raises taking an access-to-procedure, counters, and Set_Exit_Status (Failure) so continuous integration notices failures. Compile tests with -gnata so contracts act as extra oracles. GNATtest can generate test skeletons from package specs.

One expectation is deliberately wrong so the failure report and the non-zero exit status can be seen. Output: Roman numeral tests: PASS 1 -> I PASS 4 -> IV PASS 1994 -> MCMXCIV PASS 3999 -> MMMCMXCIX PASS length of 3888 FAIL deliberately wrong expectation: expected "VIIII", got "IX" PASS 0 is rejected Passed: 6, Failed: 1 [process exited with status 1]

• AUnit: Test_Case, Assert, suites, harness (via Alire). • Generic assertions reuse one routine for many types. • Non-zero exit status on failure for CI. • Build tests with -gnata so contracts are checked.

-- file: mini_test.ads
--  A tiny unit-test harness (the ideas behind AUnit, in 30 lines)
package Mini_Test is
   generic
      type T (<>) is private;              --  (<>) so String can be tested too
      with function Image (X : T) return String;
   procedure Assert_Equal (Name : String; Actual, Expected : T);

   procedure Assert_Raises (Name : String; Action : not null access procedure);
   procedure Report;
   function Failures return Natural;
end Mini_Test;

-- file: roman.ads
package Roman is
   subtype Roman_Range is Positive range 1 .. 3999;
   function To_Roman (N : Roman_Range) return String;
end Roman;

-- file: mini_test.adb
with Ada.Text_IO; use Ada.Text_IO;

package body Mini_Test is
   Passed, Failed : Natural := 0;

   procedure Assert_Equal (Name : String; Actual, Expected : T) is
   begin
      if Actual = Expected then
         Passed := Passed + 1;
         Put_Line ("  PASS " & Name);
      else
         Failed := Failed + 1;
         Put_Line ("  FAIL " & Name & ": expected " & Image (Expected) & ", got " & Image (Actual));
      end if;
   end Assert_Equal;

   procedure Assert_Raises (Name : String; Action : not null access procedure) is
   begin
      Ac
…

Tracebacks

Compile with -g and bind with -Es (symbolic) or -E (addresses) to have unhandled exceptions print a call stack. Inside a handler, GNAT.Traceback.Symbolic.Symbolic_Traceback (E) returns the same text for logging, and Ada.Exceptions.Exception_Information (E) includes it. Addresses differ between runs; function names and line numbers are what matter.

Real output from crash.adb (a Process loop calling Lookup (4) on a 1 .. 3 table), built with -g and -bargs -Es. The directory path and the trailing Windows system-DLL frames are shortened.

• -g + binder -Es → symbolic traceback on unhandled exceptions. • GNAT.Traceback.Symbolic for logging inside handlers. • Exception_Information (E) for full details.

$ ./crash.exe

raised CONSTRAINT_ERROR : crash.adb:7 range check failed
Load address: 0x7ff7cd490000
[...\tbdemo\crash.exe]
0x7ff7cd491b5d Crash.Lookup at crash.adb:7
0x7ff7cd491be8 Crash.Process at crash.adb:14
0x7ff7cd491ba3 Crash at crash.adb:18
0x7ff7cd491ac0 Main at b~crash.adb:193
0x7ff7cd4913b8 __tmainCRTStartup at ???
0x7ff7cd49107e mainCRTStartup at ???
...
$ echo $?
1

Debugging with GDB

GDB understands Ada: 'catch exception' (or 'catch exception Constraint_Error', 'catch handlers', 'catch assert') stops when an exception is raised, before the stack unwinds; 'break file.adb:14' and 'break Crash.Process' set breakpoints; 'bt' shows the stack; 'print Table', 'print Table(2)', 'print R.Field' use Ada syntax; 'info tasks' and 'task 2' inspect tasks. Build with -g -O0 for the most faithful debugging.

A real GDB session (frames 1–5 inside the runtime shortened). The catchpoint shows the argument value i=4 that caused the failure.

• catch exception [Name] / catch handlers / catch assert. • break Unit.Subprogram or file:line; bt; print with Ada syntax. • info tasks / task N for concurrent programs. • Also: gcov (-fprofile-arcs -ftest-coverage), GNATcoverage, GNAT SAS, Valgrind-free safety via checks.

-- building tests without -gnata -- enable assertions/contracts in test builds

Otherwise Pre/Post and predicates are not checked during tests.

exception when others => Put_Line ("error");

exception when E : others => Put_Line (Exception_Information (E)); -- with -Es: includes the traceback

Log exception details; a bare message is useless when debugging.

-- test runner that prints FAIL but exits 0 Set_Exit_Status (Failure);

CI decides success from the exit status.

Which binder switch gives symbolic tracebacks? A. -g B. -Es C. -O2 D. -gnatwa B. (with -g at compile time)

Which GDB command stops when any Ada exception is raised? A. break exception B. catch exception C. watch raise D. trap all

What is the xUnit framework for Ada? A. AdaTest B. AUnit C. GTest-Ada D. SPARKUnit

Why use a generic Assert_Equal? A. Speed B. One routine checks values of any type given an Image function C. It's required by AUnit D. To avoid exceptions Trace the test harness run.

Real output: Roman numeral tests: PASS 1 -> I PASS 4 -> IV PASS 1994 -> MCMXCIV PASS 3999 -> MMMCMXCIX PASS length of 3888 FAIL deliberately wrong expectation: expected "VIIII", got "IX" PASS 0 is rejected Passed: 6, Failed: 1 [process exited with status 1]

$ gdb -batch -ex "catch exception" -ex run -ex bt ./crash.exe
Catchpoint 1: all Ada exceptions

Catchpoint 1, CONSTRAINT_ERROR (crash.adb:7 range check failed) at 0x00007ff7cd491b5f in crash.lookup (i=4) at crash.adb:7
7	      return Table (Index (I));        --  fails for I = 4
#0  <__gnat_debug_raise_exception> (...) at s-excdeb.adb:38
...
#6  0x00007ff7cd491b5f in crash.lookup (i=4) at crash.adb:7
#7  0x00007ff7cd491bea in crash.process (n=4) at crash.adb:14
#8  0x00007ff7cd491ba5 in crash () at crash.adb:18
-- file: mini_test.ads
--  A tiny unit-test harness (the ideas behind AUnit, in 30 lines)
package Mini_Test is
   generic
      type T (<>) is private;              --  (<>) so String can be tested too
      with function Image (X : T) return String;
   procedure Assert_Equal (Name : String; Actual, Expected : T);

   procedure Assert_Raises (Name : String; Action : not null access procedure);
   procedure Report;
   function Failures return Natural;
end Mini_Test;

-- file: roman.ads
package Roman is
   subtype Roman_Range is Positive range 1 .. 3999;
   function To_Roman (N : Roman_Range) return String;
end Roman;

-- file: mini_test.adb
with Ada.Text_IO; use Ada.Text_IO;

package body Mini_Test is
   Passed, Failed : Natural := 0;

   procedure Assert_Equal (Name : String; Actual, Expected : T) is
   begin
      if Actual = Expected then
         Passed := Passed + 1;
         Put_Line ("  PASS " & Name);
      else
         Failed := Failed + 1;
         Put_Line ("  FAIL " & Name & ": expected " & Image (Expected) & ", got " & Image (Actual));
      end if;
   end Assert_Equal;

   procedure Assert_Raises (Name : String; Action : not null access procedure) is
   begin
      Ac
…