diff mbox series

[5/7] libdiagnostics v3: test suite

Message ID 20240815181156.1815075-6-dmalcolm@redhat.com
State New
Headers show
Series v3 of libdiagnostics | expand

Commit Message

David Malcolm Aug. 15, 2024, 6:11 p.m. UTC
Changed in v3:
* split out the C and C++ API tests into this patch
* heavily rewritten libdiagnostics.exp; added support for Python tests
* tests updated for API changes, rewritten and extended

gcc/testsuite/ChangeLog:
	* libdiagnostics.dg/libdiagnostics.exp: New, adapted from jit.exp.
	* libdiagnostics.dg/sarif.py: New.
	* libdiagnostics.dg/test-dump.c: New test.
	* libdiagnostics.dg/test-error-c.py: New test.
	* libdiagnostics.dg/test-error-with-note-c.py: New test.
	* libdiagnostics.dg/test-error-with-note.c: New test.
	* libdiagnostics.dg/test-error-with-note.cc: New test.
	* libdiagnostics.dg/test-error.c: New test.
	* libdiagnostics.dg/test-error.cc: New test.
	* libdiagnostics.dg/test-fix-it-hint-c.py: New test.
	* libdiagnostics.dg/test-fix-it-hint.c: New test.
	* libdiagnostics.dg/test-fix-it-hint.cc: New test.
	* libdiagnostics.dg/test-helpers++.h: New test.
	* libdiagnostics.dg/test-helpers.h: New test.
	* libdiagnostics.dg/test-labelled-ranges.c: New test.
	* libdiagnostics.dg/test-labelled-ranges.cc: New test.
	* libdiagnostics.dg/test-labelled-ranges.py: New test.
	* libdiagnostics.dg/test-logical-location-c.py: New test.
	* libdiagnostics.dg/test-logical-location.c: New test.
	* libdiagnostics.dg/test-metadata-c.py: New test.
	* libdiagnostics.dg/test-metadata.c: New test.
	* libdiagnostics.dg/test-multiple-lines-c.py: New test.
	* libdiagnostics.dg/test-multiple-lines.c: New test.
	* libdiagnostics.dg/test-no-column-c.py: New test.
	* libdiagnostics.dg/test-no-column.c: New test.
	* libdiagnostics.dg/test-no-diagnostics-c.py: New test.
	* libdiagnostics.dg/test-no-diagnostics.c: New test.
	* libdiagnostics.dg/test-note-with-fix-it-hint-c.py: New test.
	* libdiagnostics.dg/test-note-with-fix-it-hint.c: New test.
	* libdiagnostics.dg/test-text-sink-options.c: New test.
	* libdiagnostics.dg/test-warning-c.py: New test.
	* libdiagnostics.dg/test-warning-with-path-c.py: New test.
	* libdiagnostics.dg/test-warning-with-path.c: New test.
	* libdiagnostics.dg/test-warning.c: New test.
	* libdiagnostics.dg/test-write-sarif-to-file-c.py: New test.
	* libdiagnostics.dg/test-write-sarif-to-file.c: New test.
	* libdiagnostics.dg/test-write-text-to-file.c: New test.

Signed-off-by: David Malcolm <dmalcolm@redhat.com>
---
 .../libdiagnostics.dg/libdiagnostics.exp      | 296 ++++++++++++++++++
 gcc/testsuite/libdiagnostics.dg/sarif.py      |  23 ++
 gcc/testsuite/libdiagnostics.dg/test-dump.c   |  69 ++++
 .../libdiagnostics.dg/test-error-c.py         |  54 ++++
 .../test-error-with-note-c.py                 |  50 +++
 .../libdiagnostics.dg/test-error-with-note.c  |  74 +++++
 .../libdiagnostics.dg/test-error-with-note.cc |  55 ++++
 gcc/testsuite/libdiagnostics.dg/test-error.c  |  59 ++++
 gcc/testsuite/libdiagnostics.dg/test-error.cc |  47 +++
 .../libdiagnostics.dg/test-fix-it-hint-c.py   |  46 +++
 .../libdiagnostics.dg/test-fix-it-hint.c      |  81 +++++
 .../libdiagnostics.dg/test-fix-it-hint.cc     |  74 +++++
 .../libdiagnostics.dg/test-helpers++.h        |  28 ++
 .../libdiagnostics.dg/test-helpers.h          |  72 +++++
 .../libdiagnostics.dg/test-labelled-ranges.c  |  69 ++++
 .../libdiagnostics.dg/test-labelled-ranges.cc |  64 ++++
 .../libdiagnostics.dg/test-labelled-ranges.py |  48 +++
 .../test-logical-location-c.py                |  37 +++
 .../libdiagnostics.dg/test-logical-location.c |  79 +++++
 .../libdiagnostics.dg/test-metadata-c.py      |  45 +++
 .../libdiagnostics.dg/test-metadata.c         |  61 ++++
 .../test-multiple-lines-c.py                  |  83 +++++
 .../libdiagnostics.dg/test-multiple-lines.c   |  76 +++++
 .../libdiagnostics.dg/test-no-column-c.py     |  35 +++
 .../libdiagnostics.dg/test-no-column.c        |  52 +++
 .../test-no-diagnostics-c.py                  |  42 +++
 .../libdiagnostics.dg/test-no-diagnostics.c   |  25 ++
 .../test-note-with-fix-it-hint-c.py           |  54 ++++
 .../test-note-with-fix-it-hint.c              |  69 ++++
 .../test-text-sink-options.c                  |  59 ++++
 .../libdiagnostics.dg/test-warning-c.py       |  54 ++++
 .../test-warning-with-path-c.py               | 108 +++++++
 .../test-warning-with-path.c                  | 125 ++++++++
 .../libdiagnostics.dg/test-warning.c          |  65 ++++
 .../test-write-sarif-to-file-c.py             |  55 ++++
 .../test-write-sarif-to-file.c                |  55 ++++
 .../test-write-text-to-file.c                 |  47 +++
 37 files changed, 2435 insertions(+)
 create mode 100644 gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
 create mode 100644 gcc/testsuite/libdiagnostics.dg/sarif.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-dump.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-with-note.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error.cc
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-helpers++.h
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-helpers.h
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-logical-location.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-metadata-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-metadata.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-no-column-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-no-column.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-warning-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-warning.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c
 create mode 100644 gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c
diff mbox series

Patch

diff --git a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
new file mode 100644
index 000000000000..d29a469ae4f8
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
@@ -0,0 +1,296 @@ 
+# Test code for libdiagnostics.so
+#
+# We will compile each of libdiagnostics.dg/test-*.{c,cc} into an executable
+# dynamically linked against libdiagnostics.so, and then run each
+# such executable.
+#
+# These executables call into the libdiagnostics.so API to emit diagnostics,
+# sometimes in text form, and other times in SARIF form.
+
+# Kludge alert:
+# We need g++_init so that it can find the stdlib include path.
+#
+# g++_init (in lib/g++.exp) uses g++_maybe_build_wrapper,
+# which normally comes from the definition of
+# ${tool}_maybe_build_wrapper within lib/wrapper.exp.
+#
+# However, for us, ${tool} is "libdiagnostics".
+# Hence we load wrapper.exp with tool == "g++", so that
+# g++_maybe_build_wrapper is defined.
+set tool g++
+load_lib wrapper.exp
+set tool libdiagnostics
+
+load_lib dg.exp
+load_lib prune.exp
+load_lib target-supports.exp
+load_lib gcc-defs.exp
+load_lib timeout.exp
+load_lib target-libpath.exp
+load_lib gcc.exp
+load_lib g++.exp
+load_lib dejagnu.exp
+load_lib target-supports-dg.exp
+load_lib valgrind.exp
+load_lib scansarif.exp
+load_lib dg-test-cleanup.exp
+
+# The default do-what keyword.
+set dg-do-what-default compile
+
+# Adapted from jit.exp.
+#
+# Execute the executable file.
+#    Returns:
+#	A "" (empty) string if everything worked, or an error message
+#	if there was a problem.
+#
+proc fixed_host_execute {args} {
+    global env
+    global text
+    global spawn_id
+
+    verbose "fixed_host_execute: $args"
+
+    set timeoutmsg "Timed out: Never got started, "
+    set timeout 100
+    set file all
+    set timetol 0
+    set arguments ""
+
+    if { [llength $args] == 0} {
+	set executable $args
+    } else {
+	set executable [lindex $args 0]
+	set params [lindex $args 1]
+    }
+
+    verbose "The executable is $executable" 2
+    if {![file exists ${executable}]} {
+	perror "The executable, \"$executable\" is missing" 0
+	return "No source file found"
+    } elseif {![file executable ${executable}]} {
+	perror "The executable, \"$executable\" is not usable" 0
+	return "Bad executable found"
+    }
+
+    verbose "params: $params" 2
+
+    # spawn the executable and look for the DejaGnu output messages from the
+    # test case.
+    # spawn -noecho -open [open "|./${executable}" "r"]
+
+    # Run under valgrind if RUN_UNDER_VALGRIND is present in the environment.
+    # Note that it's best to configure gcc with --enable-valgrind-annotations
+    # when testing under valgrind.
+    set run_under_valgrind [info exists env(RUN_UNDER_VALGRIND)]
+    if $run_under_valgrind {
+	set valgrind_logfile "${executable}.valgrind.txt"
+	set valgrind_params {"valgrind"}
+	lappend valgrind_params "--leak-check=full"
+	lappend valgrind_params "--log-file=${valgrind_logfile}"
+    } else {
+	set valgrind_params {}
+    }
+    verbose "valgrind_params: $valgrind_params" 2
+
+    set args ${valgrind_params}
+    lappend args "./${executable}"
+    set args [concat $args ${params}]
+    verbose "args: $args" 2
+
+    set status [catch "exec -keepnewline $args" exe_output]
+    verbose "Test program returned $exe_output" 2
+ 
+    if $run_under_valgrind {
+	upvar 2 name name
+	parse_valgrind_logfile $name $valgrind_logfile fail
+    }
+
+    # We don't do prune_gcc_output here, as we want
+    # to check *exactly* what we get from libdiagnostics
+
+    return $exe_output
+}
+
+# (end of code from dejagnu.exp)
+
+# GCC_UNDER_TEST is needed by gcc_target_compile
+global GCC_UNDER_TEST
+if ![info exists GCC_UNDER_TEST] {
+    set GCC_UNDER_TEST "[find_gcc]"
+}
+
+g++_init
+
+# Initialize dg.
+dg-init
+
+# Gather a list of all tests.
+
+# C and C++ tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.{c,c++}
+set c_tests [find $srcdir/$subdir test-*.c]
+set cxx_tests [find $srcdir/$subdir test-*.cc]
+set tests [concat $c_tests $cxx_tests]
+
+verbose "tests: $tests"
+
+# Expand "SRCDIR" within ARG to the location of the top-level
+# src directory
+
+proc diagnostics-expand-vars {arg} {
+    verbose "diagnostics-expand-vars: $arg"
+    global srcdir
+    verbose " srcdir: $srcdir"
+    # "srcdir" is that of the gcc/testsuite directory, so
+    # we need to go up two levels.
+    set arg [string map [list "SRCDIR" $srcdir/../..] $arg]
+    verbose " new arg: $arg"
+    return $arg
+}
+
+# Parameters used when invoking the executables built from the test cases.
+
+global diagnostics-exe-params
+set diagnostics-exe-params {}
+
+# Set "diagnostics-exe-params", expanding "SRCDIR" in each arg to the location of
+# the top-level srcdir.
+
+proc dg-diagnostics-set-exe-params { args } {
+    verbose "dg-diagnostics-set-exe-params: $args"
+
+    global diagnostics-exe-params
+    set diagnostics-exe-params {}
+    # Skip initial arg (line number)
+    foreach arg [lrange $args 1 [llength $args] ] {
+	lappend diagnostics-exe-params [diagnostics-expand-vars $arg]
+    }
+}
+
+proc libdiagnostics-dg-test { prog do_what extra_tool_flags } {
+    verbose "within libdiagnostics-dg-test..."
+    verbose "  prog: $prog"
+    verbose "  do_what: $do_what"
+    verbose "  extra_tool_flags: $extra_tool_flags"
+
+    global dg-do-what-default
+    set dg-do-what [list ${dg-do-what-default} "" P]
+
+    # If we're not supposed to try this test on this target, we're done.
+    if { [lindex ${dg-do-what} 1] == "N" } {
+	unsupported "$name"
+	verbose "$name not supported on this target, skipping it" 3
+	return
+    }
+
+    # Determine what to name the built executable.
+    #
+    # We simply append .exe to the filename, e.g.
+    #  "test-foo.c.exe"
+    # since some testcases exist in both
+    #  "test-foo.c" and
+    #  "test-foo.cc"
+    # variants, and we don't want them to clobber each other's
+    # executables.
+    #
+    # This also ensures that the source name makes it into the
+    # pass/fail output, so that we can distinguish e.g. which test-foo
+    # is failing.
+    set output_file "[file tail $prog].exe"
+    verbose "output_file: $output_file"
+
+    # Create the test executable:
+    set extension [file extension $prog]
+    if {$extension == ".cc"} {
+	set compilation_function "g++_target_compile"
+    } else {
+	set compilation_function "gcc_target_compile"
+    }
+    set options "{additional_flags=$extra_tool_flags}"
+    verbose "compilation_function=$compilation_function"
+    verbose "options=$options"
+
+    set comp_output [$compilation_function $prog $output_file \
+			 "executable" $options]
+    upvar 1 name name
+    if ![libdiagnostics_check_compile "$name" "initial compilation" \
+	    $output_file $comp_output] then {
+      return
+    }
+
+    # Run the test executable.
+
+    # We need to set LD_LIBRARY_PATH so that the test files can find
+    # libdiagnostics.so
+    # Do this using set_ld_library_path_env_vars from target-libpath.exp
+    # We will restore the old value later using
+    # restore_ld_library_path_env_vars.
+
+    # Unfortunately this API only supports a single saved value, rather
+    # than a stack, and g++_init has already called into this API,
+    # injecting the appropriate value for LD_LIBRARY_PATH for finding
+    # the built copy of libstdc++.
+    # Hence the call to restore_ld_library_path_env_vars would restore
+    # the *initial* value of LD_LIBRARY_PATH, and attempts to run
+    # a C++ testcase after running any prior testcases would thus look
+    # in the wrong place for libstdc++.  This led to failures at startup
+    # of the form:
+    #   ./tut01-hello-world.cc.exe: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./tut01-hello-world.cc.exe)
+    # when the built libstdc++ is more recent that the system libstdc++.
+    #
+    # As a workaround, reset the variable "orig_environment_saved" within
+    # target-libpath.exp, so that the {set|restore}_ld_library_path_env_vars
+    # API saves/restores the current value of LD_LIBRARY_PATH (as set up
+    # by g++_init).
+    global orig_environment_saved
+    set orig_environment_saved 0
+
+    global ld_library_path
+    global base_dir
+    set ld_library_path "$base_dir/../../"
+    set_ld_library_path_env_vars
+
+    global diagnostics-exe-params
+    set args ${diagnostics-exe-params}
+    set diagnostics-exe-params {}
+
+    set exe_output [fixed_host_execute $output_file $args ]
+    verbose "exe_output: $exe_output"
+
+    restore_ld_library_path_env_vars
+
+    # Analyze the output from the executable.  To some what extent this
+    # is duplicating prune_gcc_output, but we're looking for *precise*
+    # output, so we can't reuse prune_gcc_output.
+
+    global testname_with_flags
+    set testname_with_flags $name
+
+    # Handle any freeform regexps.
+    set exe_output [handle-dg-regexps $exe_output]
+
+    # Call into multiline.exp to handle any multiline output directives.
+    set exe_output [handle-multiline-outputs $exe_output]
+
+    # Normally we would return $exe_output and $output_file to the
+    # caller, which would delete $output_file, the generated executable.
+    # If we need to debug, it's handy to be able to suppress this behavior,
+    # keeping the executable around.
+    
+    global env
+    set preserve_executables [info exists env(PRESERVE_EXECUTABLES)]
+    if $preserve_executables {
+	set output_file ""
+    }
+
+    return [list $exe_output $output_file]
+}
+
+set DEFAULT_CFLAGS "-I$srcdir/.. -ldiagnostics -g -Wall -Werror"
+
+# Main loop.  This will invoke jig-dg-test on each test-*.c file.
+dg-runtest $tests "" $DEFAULT_CFLAGS
+
+# All done.
+dg-finish
diff --git a/gcc/testsuite/libdiagnostics.dg/sarif.py b/gcc/testsuite/libdiagnostics.dg/sarif.py
new file mode 100644
index 000000000000..7daf35b58190
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/sarif.py
@@ -0,0 +1,23 @@ 
+import json
+import os
+
+def sarif_from_env():
+    # return parsed JSON content a SARIF_PATH file
+    json_filename = os.environ['SARIF_PATH']
+    json_filename += '.sarif'
+    print('json_filename: %r' % json_filename)
+    with open(json_filename) as f:
+        json_data = f.read()
+    return json.loads(json_data)
+
+def get_location_artifact_uri(location):
+    return location['physicalLocation']['artifactLocation']['uri']
+
+def get_location_physical_region(location):
+    return location['physicalLocation']['region']
+
+def get_location_snippet_text(location):
+    return location['physicalLocation']['contextRegion']['snippet']['text']
+
+def get_location_relationships(location):
+    return location['relationships']
diff --git a/gcc/testsuite/libdiagnostics.dg/test-dump.c b/gcc/testsuite/libdiagnostics.dg/test-dump.c
new file mode 100644
index 000000000000..9f0576d5cd3e
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-dump.c
@@ -0,0 +1,69 @@ 
+/* Usage example of dump API.  */
+
+#include "libdiagnostics.h"
+
+const int line_num = 42;
+
+int
+main ()
+{
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, "foo.c", "c");
+
+  fprintf (stderr, "file: ");
+  diagnostic_manager_debug_dump_file (diag_mgr, file, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+file: file(name="foo.c", sarif_source_language="c")
+     { dg-end-multiline-output "" } */
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+					  loc_start,
+					  loc_start,
+					  loc_end);
+
+  fprintf (stderr, "loc_start: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_start, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_start: foo.c:42:8:
+     { dg-end-multiline-output "" } */
+
+  fprintf (stderr, "loc_end: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_end, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_end: foo.c:42:19:
+     { dg-end-multiline-output "" } */
+
+  fprintf (stderr, "loc_range: ");
+  diagnostic_manager_debug_dump_location (diag_mgr, loc_range, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+loc_range: foo.c:42:8:
+     { dg-end-multiline-output "" } */
+
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+					       DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+					       NULL, /* parent */
+					       "test_short_name",
+					       "test_qualified_name",
+					       "test_decorated_name");
+
+  fprintf (stderr, "logical_loc: ");
+  diagnostic_manager_debug_dump_logical_location (diag_mgr, logical_loc, stderr);
+  fprintf (stderr, "\n");
+  /* { dg-begin-multiline-output "" }
+logical_loc: logical_location(kind=function, short_name="test_short_name", fully_qualified_name="test_qualified_name", decorated_name="test_decorated_name")
+     { dg-end-multiline-output "" } */
+
+  diagnostic_manager_release (diag_mgr);
+  return 0;
+};
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-c.py b/gcc/testsuite/libdiagnostics.dg/test-error-c.py
new file mode 100644
index 000000000000..84e3899f3b02
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error-c.py
@@ -0,0 +1,54 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 17
+expected_file_name = 'test-error.c'
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == False
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert 'PRINT' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py b/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py
new file mode 100644
index 000000000000..bb352fbac848
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note-c.py
@@ -0,0 +1,50 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 18
+
+def test_sarif_output_for_note(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-error-with-note.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-error-with-note.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
+
+    assert len(results[0]['relatedLocations']) == 1
+    note = results[0]['relatedLocations'][0]
+    phys_loc = note['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-error-with-note.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
+    assert note['message']['text'] == 'have you looked behind the couch?'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c
new file mode 100644
index 000000000000..11a3b998c3e1
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.c
@@ -0,0 +1,74 @@ 
+/* Example of emitting an error with an associated note.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:18:8: error: can't find 'foo'
+    6 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+PATH/test-error-with-note.c:18:8: note: have you looked behind the couch?
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-error-with-note.c.exe",
+	      "test-error-with-note.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic_manager_begin_group (diag_mgr);
+  
+  diagnostic *err = diagnostic_begin (diag_mgr,
+				      DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (err, loc_range);
+  diagnostic_finish (err, "can't find %qs", "foo");
+
+  diagnostic *note = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_NOTE);
+  diagnostic_set_location (note, loc_range);
+  diagnostic_finish (note, "have you looked behind the couch?");
+
+  diagnostic_manager_end_group (diag_mgr);
+
+  return end_test ();
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error-with-note.c:18:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   18 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-error-with-note.c:18:8: note: have you looked behind the couch." } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-error-with-note.c "test-error-with-note-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
new file mode 100644
index 000000000000..e211297c5520
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
@@ -0,0 +1,55 @@ 
+/* C++ example of emitting an error with an associated note.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:17:8: error: can't find 'foo'
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+PATH/test-error-with-note.c:17:8: note: have you looked behind the couch?
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+  auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+  auto loc_range = mgr.new_location_from_range (loc_start,
+						loc_start,
+						loc_end);
+
+  libdiagnostics::group g (mgr);
+  
+  auto err (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  err.set_location (loc_range);
+  err.finish ("can't find %qs", "foo");
+
+  auto note = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_NOTE);
+  note.set_location (loc_range);
+  note.finish ("have you looked behind the couch?");
+
+  return 0;
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error-with-note.cc:17:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-error-with-note.cc:17:8: note: have you looked behind the couch\\\?" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.c b/gcc/testsuite/libdiagnostics.dg/test-error.c
new file mode 100644
index 000000000000..081dfb8e2dbb
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error.c
@@ -0,0 +1,59 @@ 
+/* Example of emitting an error.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:6: error: can't find 'foo'
+    6 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-error.c.exe",
+	      "test-error.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, main_file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, main_file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  return end_test ();
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error.c:17:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-error.c "test-error-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.cc b/gcc/testsuite/libdiagnostics.dg/test-error.cc
new file mode 100644
index 000000000000..6f919e473f4d
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error.cc
@@ -0,0 +1,47 @@ 
+/* C++ example of emitting an error.
+
+   Intended output is similar to:
+
+PATH/test-error.cc:16:8: error: can't find 'foo'
+   16 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+  auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+  auto loc_range = mgr.new_location_from_range (loc_start,
+						loc_start,
+						loc_end);
+
+  libdiagnostics::diagnostic d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  d.set_location (loc_range);
+  d.finish ("can't find %qs", "foo");
+
+  return 0;
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-error.cc:16:8: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   16 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py
new file mode 100644
index 000000000000..a70b04d22cd3
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint-c.py
@@ -0,0 +1,46 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_with_fixes(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-fix-it-hint.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "unknown field 'colour'; did you mean 'color'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == 19
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == 19
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+
+    assert len(results[0]['fixes']) == 1
+    fix = results[0]['fixes'][0]
+    assert len(fix['artifactChanges']) == 1
+    change = fix['artifactChanges'][0]
+    assert change['artifactLocation']['uri'].endswith('test-fix-it-hint.c')
+    assert len(change['replacements']) == 1
+    replacement = change['replacements'][0]
+    assert replacement['deletedRegion'] == phys_loc['region']
+    assert replacement['insertedContent']['text'] == 'color'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c
new file mode 100644
index 000000000000..723969ffdd04
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.c
@@ -0,0 +1,81 @@ 
+/* Example of a fix-it hint, including patch generation.
+
+   Intended output is similar to:
+
+PATH/test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF, and a generated patch (on stderr) to
+   make the change.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-fix-it-hint.c.exe",
+	      "test-fix-it-hint.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 13, 18);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_token);
+
+  diagnostic_add_fix_it_hint_replace (d, loc_token, "color");
+  
+  diagnostic_finish (d, "unknown field %qs; did you mean %qs", "colour", "color");
+
+  diagnostic_manager_write_patch (diag_mgr, stderr);
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-fix-it-hint.c:19:13: error: unknown field 'colour'; did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* Verify the output from diagnostic_manager_write_patch.
+   We expect the patch to begin with a header, containing this
+   source filename, via an absolute path.
+   Given the path, we can only capture it via regexps.  */
+/* { dg-regexp "\\-\\-\\- .*" } */
+/* { dg-regexp "\\+\\+\\+ .*" } */
+/* Use #if 0/#endif rather than comments, to allow the text to contain
+   a comment.  */
+#if 0
+{ dg-begin-multiline-output "" }
+@@ -16,7 +16,7 @@
+ /*
+ _________11111111112
+ 12345678901234567890
+-  return p->colour;
++  return p->color;
+ */
+ const int line_num = __LINE__ - 2;
+ 
+{ dg-end-multiline-output "" }
+#endif
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-fix-it-hint.c "test-fix-it-hint-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
new file mode 100644
index 000000000000..92c7f07117bd
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
@@ -0,0 +1,74 @@ 
+/* C++ example of a fix-it hint, including patch generation.
+
+   Intended output is similar to:
+
+PATH/test-fix-it-hint.cc:19:13: error: unknown field 'colour'; did you mean 'color'
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF, and a generated patch (on stderr) to
+   make the change.  */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  libdiagnostics::manager mgr;
+
+  auto file = mgr.new_file (__FILE__, "c");
+
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+
+  auto loc_token = make_range (mgr, file, line_num, 13, 18);
+
+  auto d = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR);
+  d.set_location (loc_token);
+
+  d.add_fix_it_hint_replace (loc_token, "color");
+  
+  d.finish ("unknown field %qs; did you mean %qs", "colour", "color");
+
+  mgr.write_patch (stderr);
+
+  return 0;
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-fix-it-hint.cc:19:13: error: unknown field 'colour'; did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   19 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* Verify the output from diagnostic_manager_write_patch.
+   We expect the patch to begin with a header, containing this
+   source filename, via an absolute path.
+   Given the path, we can only capture it via regexps.  */
+/* { dg-regexp "\\-\\-\\- .*" } */
+/* { dg-regexp "\\+\\+\\+ .*" } */
+/* Use #if 0/#endif rather than comments, to allow the text to contain
+   a comment.  */
+#if 0
+{ dg-begin-multiline-output "" }
+@@ -16,7 +16,7 @@
+ /*
+ _________11111111112
+ 12345678901234567890
+-  return p->colour;
++  return p->color;
+ */
+ const int line_num = __LINE__ - 2;
+ 
+{ dg-end-multiline-output "" }
+#endif
diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers++.h b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h
new file mode 100644
index 000000000000..c8ff2def1ffa
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h
@@ -0,0 +1,28 @@ 
+/* Common utility code shared between test cases.  */
+
+#ifndef TEST_HELPERSPP_H
+#define TEST_HELPERSPP_H
+
+namespace libdiagnostics {
+
+inline physical_location
+make_range (manager &mgr,
+	    file f,
+	    line_num_t line_num,
+	    column_num_t start_column,
+	    column_num_t end_column)
+{
+  auto loc_start = mgr.new_location_from_file_line_column (f,
+							   line_num,
+							   start_column);
+  auto loc_end = mgr.new_location_from_file_line_column (f,
+							 line_num,
+							 end_column);
+  return mgr.new_location_from_range (loc_start,
+				      loc_start,
+				      loc_end);
+}
+
+} // namespace libdiagnostics
+
+#endif /* #ifndef TEST_HELPERSPP_H */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers.h b/gcc/testsuite/libdiagnostics.dg/test-helpers.h
new file mode 100644
index 000000000000..c06c0637b940
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-helpers.h
@@ -0,0 +1,72 @@ 
+/* Common utility code shared between test cases.  */
+
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+const diagnostic_physical_location *
+make_range (diagnostic_manager *diag_mgr,
+	    const diagnostic_file *file,
+	    diagnostic_line_num_t line_num,
+	    diagnostic_column_num_t start_column,
+	    diagnostic_column_num_t end_column)
+{
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     file,
+							     line_num,
+							     start_column);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     file,
+							     line_num,
+							     end_column);
+  return diagnostic_manager_new_location_from_range (diag_mgr,
+						     loc_start,
+						     loc_start,
+						     loc_end);
+}
+
+/* A begin_test/end_test pair to consolidate the code shared by tests:
+   create a diagnostic_manager, a main file, a text sink, and a SARIF sink,
+   and clean these up after emitting zero or more diagnostics.  */
+
+static diagnostic_manager *diag_mgr;
+static const diagnostic_file *main_file;
+static FILE *sarif_outfile;
+
+static void
+begin_test (const char *tool_name,
+	    const char *sarif_output_name,
+	    const char *main_file_name,
+	    const char *source_language)
+{
+  diag_mgr = diagnostic_manager_new ();
+
+  /* We need to set this for generated .sarif files to validat
+     against the schema.  */
+  diagnostic_manager_set_tool_name (diag_mgr, tool_name);
+
+  main_file = diagnostic_manager_new_file (diag_mgr,
+					   main_file_name,
+					   source_language);
+
+  diagnostic_manager_add_text_sink (diag_mgr, stderr,
+				    DIAGNOSTIC_COLORIZE_IF_TTY);
+  sarif_outfile = fopen (sarif_output_name, "w");
+  if (sarif_outfile)
+    diagnostic_manager_add_sarif_sink (diag_mgr,
+				       sarif_outfile,
+				       main_file,
+				       DIAGNOSTIC_SARIF_VERSION_2_1_0);
+}
+
+static int
+end_test (void)
+{
+  diagnostic_manager_release (diag_mgr);
+  if (sarif_outfile)
+    fclose (sarif_outfile);
+  return 0;
+}
+
+#endif /* #ifndef TEST_HELPERS_H */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c
new file mode 100644
index 000000000000..41bfbdc62feb
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.c
@@ -0,0 +1,69 @@ 
+/* Example of multiple locations, with labelling of ranges.
+
+   Intended output is similar to:
+
+PATH/test-labelled-ranges.c:9:6: error: mismatching types: 'int' and 'const char *'
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________11111111112
+12345678901234567890
+  42 + "foo"
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-labelled-ranges.c.exe",
+	      "test-labelled-ranges.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_operator
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     6);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_operator);
+  diagnostic_add_location_with_label (d,
+				      make_range (diag_mgr,
+						  main_file,
+						  line_num, 3, 4),
+				      "int");
+  diagnostic_add_location_with_label (d,
+				      make_range (diag_mgr,
+						  main_file,
+						  line_num, 8, 12),
+				      "const char *");
+  
+  diagnostic_finish (d, "mismatching types: %qs and %qs", "int", "const char *");
+  
+  return end_test ();
+}
+
+/* Check the output from the text sink.  */
+/* { dg-regexp "\[^\n\r\]+test-labelled-ranges.c:19:6: error: mismatching types: 'int' and 'const char \\*'" } */
+/* { dg-begin-multiline-output "" }
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-labelled-ranges.c "test-labelled-ranges.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
new file mode 100644
index 000000000000..1c1c050e3042
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
@@ -0,0 +1,64 @@ 
+/* C++ example of multiple locations, with labelling of ranges.
+
+   Intended output is similar to:
+
+PATH/test-labelled-ranges.cc:19:6: error: mismatching types: 'int' and 'const char *'
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+  42 + "foo"
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *sarif_outfile;
+  libdiagnostics::manager mgr;
+  mgr.set_tool_name ("test-labelled-ranges.cc.exe");
+
+  libdiagnostics::file file = mgr.new_file (__FILE__, "c");
+  
+  mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+  sarif_outfile = fopen ("test-labelled-ranges.cc.sarif", "w");
+  if (sarif_outfile)
+    mgr.add_sarif_sink (sarif_outfile, file, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+  auto loc_operator = mgr.new_location_from_file_line_column (file, line_num, 6);
+
+  auto d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+  d.set_location (loc_operator);
+  d.add_location_with_label (make_range (mgr, file, line_num, 3, 4),
+			     "int");
+  d.add_location_with_label (make_range (mgr, file, line_num, 8, 12),
+			     "const char *");
+  d.finish ("mismatching types: %qs and %qs", "int", "const char *");
+  
+  return 0;
+}
+
+/* Check the output from the text sink.  */
+/* { dg-regexp "\[^\n\r\]+test-labelled-ranges.cc:19:6: error: mismatching types: 'int' and 'const char \\*'" } */
+/* { dg-begin-multiline-output "" }
+   19 |   42 + "foo"
+      |   ~~ ^ ~~~~~
+      |   |    |
+      |   int  const char *
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-labelled-ranges.cc "test-labelled-ranges.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py
new file mode 100644
index 000000000000..f2f13867ce9f
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.py
@@ -0,0 +1,48 @@ 
+# Verify the SARIF output of test-labelled-ranges.{c,cc}
+
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] \
+        == "mismatching types: 'int' and 'const char *'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['region']['startLine'] == 19
+    assert phys_loc['region']['startColumn'] == 6
+    assert phys_loc['region']['endColumn'] == 7
+    assert phys_loc['contextRegion']['startLine'] == 19
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  42 + "foo"\n'
+
+    annotations = location['annotations']
+    assert len(annotations) == 2
+
+    assert annotations[0]['startLine'] == 19
+    assert annotations[0]['startColumn'] == 3
+    assert annotations[0]['endColumn'] == 5
+    assert annotations[0]['message']['text'] == 'int'
+
+    assert annotations[1]['startLine'] == 19
+    assert annotations[1]['startColumn'] == 8
+    assert annotations[1]['endColumn'] == 13
+    assert annotations[1]['message']['text'] == 'const char *'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py b/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py
new file mode 100644
index 000000000000..5344b05578ca
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-logical-location-c.py
@@ -0,0 +1,37 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_with_logical_location(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-logical-location.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'error'
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "can't find 'foo'"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+
+    assert len(location['logicalLocations']) == 1
+    logical_loc = location['logicalLocations'][0]
+    assert logical_loc['name'] == 'test_short_name'
+    assert logical_loc['fullyQualifiedName'] == 'test_qualified_name'
+    assert logical_loc['decoratedName'] == 'test_decorated_name'
+    assert logical_loc['kind'] == 'function'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-logical-location.c b/gcc/testsuite/libdiagnostics.dg/test-logical-location.c
new file mode 100644
index 000000000000..815296ff8be9
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-logical-location.c
@@ -0,0 +1,79 @@ 
+/* Example of using a logical location.
+
+   Intended output is similar to:
+
+In function 'test_qualified_name':
+PATH/test-error-with-note.c:18:8: error: can't find 'foo'
+   18 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-logical-location.c.exe",
+	      "test-logical-location.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+					  loc_start,
+					  loc_start,
+					  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+					       DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+					       NULL, /* parent */
+					       "test_short_name",
+					       "test_qualified_name",
+					       "test_decorated_name");
+
+  diagnostic_set_logical_location (d, logical_loc);
+
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  return end_test ();
+}
+
+/* Check the output from the text sink.  */
+/* { dg-begin-multiline-output "" }
+In function 'test_qualified_name':
+   { dg-end-multiline-output "" } */
+/* { dg-regexp "\[^\n\r\]+test-logical-location.c:18:8: error: can't find 'foo'" } */
+/* { dg-begin-multiline-output "" }
+   18 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-logical-location.c "test-logical-location-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py b/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py
new file mode 100644
index 000000000000..139f940ea453
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-metadata-c.py
@@ -0,0 +1,45 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output_metadata(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'FooChecker'
+    assert tool['driver']['fullName'] == 'FooChecker 0.1 (en_US)'
+    assert tool['driver']['version'] == '0.1'
+    assert tool['driver']['informationUri'] == 'https://www.example.com/0.1/'
+
+    taxonomies = run["taxonomies"]
+    assert len(taxonomies) == 1
+
+    cwe = taxonomies[0]
+    assert cwe['name'] == 'CWE'
+    assert cwe['version'] == '4.7'
+    assert cwe['organization'] == 'MITRE'
+    assert cwe['shortDescription']['text'] \
+        == 'The MITRE Common Weakness Enumeration'
+    assert len(cwe['taxa']) == 1
+    assert cwe['taxa'][0]['id'] == '242'
+    assert cwe['taxa'][0]['helpUri'] \
+        == 'https://cwe.mitre.org/data/definitions/242.html'
+    
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] == "never use 'gets'"
diff --git a/gcc/testsuite/libdiagnostics.dg/test-metadata.c b/gcc/testsuite/libdiagnostics.dg/test-metadata.c
new file mode 100644
index 000000000000..7881c9eca3e8
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-metadata.c
@@ -0,0 +1,61 @@ 
+/* Example of setting a CWE and adding extra metadata.
+
+   Intended output is similar to:
+
+PATH/test-metadata.c:21:3: warning: never use 'gets' [CWE-242] [STR34-C]
+   21 |   gets (buf);
+      |   ^~~~~~~~~~
+
+   where the metadata tags are linkified in a sufficiently capable terminal,
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________11111111112
+12345678901234567890
+void test_cwe (void)
+{
+  char buf[1024];
+  gets (buf);
+}
+*/
+const int line_num = __LINE__ - 3;
+
+int
+main ()
+{
+  begin_test ("FooChecker",
+	      "test-metadata.c.sarif",
+	      __FILE__, "c");
+
+  diagnostic_manager_set_full_name (diag_mgr, "FooChecker 0.1 (en_US)");
+  diagnostic_manager_set_version_string (diag_mgr, "0.1");
+  diagnostic_manager_set_version_url (diag_mgr, "https://www.example.com/0.1/");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 3, 12);
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_token);
+  diagnostic_set_cwe (d, 242); /* CWE-242: Use of Inherently Dangerous Function.  */
+  diagnostic_add_rule (d, "STR34-C", "https://example.com/");
+  
+  diagnostic_finish (d, "never use %qs", "gets");
+
+  return end_test ();
+}
+
+/* { dg-regexp "\[^\n\r\]+test-metadata.c:21:3: warning: never use 'gets' \\\[CWE-242\\\] \\\[STR34-C\\\]" } */
+/* { dg-begin-multiline-output "" }
+   21 |   gets (buf);
+      |   ^~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-metadata.c "test-metadata-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py
new file mode 100644
index 000000000000..f54984195be7
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines-c.py
@@ -0,0 +1,83 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-multiple-lines.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] == "missing comma"
+    assert len(result['locations']) == 1
+
+    # The primary location should be that of the missing comma
+    location = result['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-multiple-lines.c')
+    assert phys_loc['region']['startLine'] == 23
+    assert phys_loc['region']['startColumn'] == 29
+    assert phys_loc['region']['endColumn'] == 30
+    assert phys_loc['contextRegion']['startLine'] == 23
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '                       "bar"\n'
+
+    assert len(location['relationships']) == 3
+    location['relationships'][0]['target'] == 0
+    location['relationships'][0]['kinds'] == ['relevant']
+    location['relationships'][1]['target'] == 1
+    location['relationships'][1]['kinds'] == ['relevant']
+    location['relationships'][2]['target'] == 2
+    location['relationships'][2]['kinds'] == ['relevant']
+
+    # We should be capturing the secondary locations in relatedLocations
+    assert len(result['relatedLocations']) == 3
+
+    rel_loc_0 = result['relatedLocations'][0]
+    assert get_location_artifact_uri(rel_loc_0) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_0) \
+        == 'const char *strs[3] = {"foo",\n'
+    assert get_location_physical_region(rel_loc_0)['startLine'] == 22
+    assert get_location_physical_region(rel_loc_0)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_0)['endColumn'] == 29
+    assert rel_loc_0['id'] == 0
+    assert 'relationships' not in rel_loc_0
+
+    rel_loc_1 = result['relatedLocations'][1]
+    assert get_location_artifact_uri(rel_loc_1) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_1) \
+        == '                       "bar"\n'
+    assert get_location_physical_region(rel_loc_1)['startLine'] == 23
+    assert get_location_physical_region(rel_loc_1)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_1)['endColumn'] == 29
+    assert rel_loc_1['id'] == 1
+    assert 'relationships' not in rel_loc_1
+
+    rel_loc_2 = result['relatedLocations'][2]
+    assert get_location_artifact_uri(rel_loc_2) \
+        .endswith('test-multiple-lines.c')
+    assert get_location_snippet_text(rel_loc_2) \
+        == '                       "baz"};\n'
+    assert get_location_physical_region(rel_loc_2)['startLine'] == 24
+    assert get_location_physical_region(rel_loc_2)['startColumn'] == 24
+    assert get_location_physical_region(rel_loc_2)['endColumn'] == 29
+    assert rel_loc_2['id'] == 2
+    assert 'relationships' not in rel_loc_2
diff --git a/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c
new file mode 100644
index 000000000000..765c42956ac0
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-multiple-lines.c
@@ -0,0 +1,76 @@ 
+/* Example of a warning with multiple locations in various source lines,
+   with an insertion fix-it hint.
+
+   Intended output is similar to:
+   
+/PATH/test-multiple-lines.c:23:29: warning: missing comma
+   22 | const char *strs[3] = {"foo",
+      |                        ~~~~~ 
+   23 |                        "bar"
+      |                        ~~~~~^
+   24 |                        "baz"};
+      |                        ~~~~~ 
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source (missing comma after "bar"):
+_________11111111112222222222
+12345678901234567890123456789
+const char *strs[3] = {"foo",
+                       "bar"
+                       "baz"};
+*/
+const int foo_line_num = __LINE__ - 4;
+
+int
+main ()
+{
+  begin_test ("test-multiple-lines.c.exe",
+	      "test-multiple-lines.c.sarif",
+	      __FILE__, "c");
+  
+  const diagnostic_physical_location *loc_comma
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     foo_line_num + 1,
+							     29);
+  const diagnostic_physical_location *loc_foo
+    = make_range (diag_mgr, main_file, foo_line_num, 24, 28);
+  const diagnostic_physical_location *loc_bar
+    = make_range (diag_mgr, main_file, foo_line_num + 1, 24, 28);
+  const diagnostic_physical_location *loc_baz
+    = make_range (diag_mgr, main_file, foo_line_num + 2, 24, 28);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_comma);
+  diagnostic_add_location (d, loc_foo);
+  diagnostic_add_location (d, loc_bar);
+  diagnostic_add_location (d, loc_baz);
+
+  diagnostic_add_fix_it_hint_insert_after (d, loc_bar, ",");
+
+  diagnostic_finish (d, "missing comma");
+
+  return end_test ();
+};
+
+/* { dg-regexp "\[^\n\r\]+test-multiple-lines.c:23:29: warning: missing comma" } */
+/* { dg-begin-multiline-output "" }
+   22 | const char *strs[3] = {"foo",
+      |                        ~~~~~ 
+   23 |                        "bar"
+      |                        ~~~~~^
+   24 |                        "baz"};
+      |                        ~~~~~ 
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-multiple-lines.c "test-multiple-lines-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py b/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py
new file mode 100644
index 000000000000..e005359e3ec0
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-no-column-c.py
@@ -0,0 +1,35 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 16
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-no-column.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-no-column.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    # We should have no column properties:
+    assert 'startColumn' not in phys_loc['region']
+    assert 'endColumn' not in phys_loc['region']
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-column.c b/gcc/testsuite/libdiagnostics.dg/test-no-column.c
new file mode 100644
index 000000000000..798e0f7bc8a2
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-no-column.c
@@ -0,0 +1,52 @@ 
+/* Example of emitting an error without a column number.
+
+   Intended output is similar to:
+
+PATH/test-error-with-note.c:6: error: can't find 'foo'
+    6 | PRINT "hello world!";
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-no-column.c.exe",
+	      "test-no-column.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc
+    = diagnostic_manager_new_location_from_file_and_line (diag_mgr,
+							  main_file,
+							  line_num);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc);
+
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-no-column.c:16: error: can't find 'foo'" }
+   { dg-begin-multiline-output "" }
+   16 | PRINT "hello world!";
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-no-column.c "test-no-column-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py
new file mode 100644
index 000000000000..b0edfdf4b0cd
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics-c.py
@@ -0,0 +1,42 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_file_name = 'test-no-diagnostics.c'
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == True
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    # We don't bother capturing the contents if there are
+    # no diagnostics to display
+    assert 'contents' not in artifacts[0]
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 0
diff --git a/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c
new file mode 100644
index 000000000000..78e186ab95ad
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-no-diagnostics.c
@@ -0,0 +1,25 @@ 
+/* Test of the "no diagnostics are emitted" case.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+int
+main ()
+{
+  begin_test ("test-no-diagnostics.c.exe",
+	      "test-no-diagnostics.c.sarif",
+	      __FILE__, "c");
+
+  /* No-op.  */
+
+  return end_test ();
+};
+
+/* There should be no output from the text sink.  */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-no-diagnostics.c "test-no-diagnostics-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py
new file mode 100644
index 000000000000..256c429420a1
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint-c.py
@@ -0,0 +1,54 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 21
+
+def test_sarif_output_with_fixes(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-note-with-fix-it-hint.c.exe'
+
+    results = run['results']
+    assert len(results) == 1
+    result = results[0]
+    assert result['ruleId'] == 'error'
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "unknown field 'colour'"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-note-with-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+
+    assert len(result['relatedLocations']) == 1
+    note = result['relatedLocations'][0]
+    phys_loc = note['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith('test-note-with-fix-it-hint.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 13
+    assert phys_loc['region']['endColumn'] == 19
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '  return p->colour;\n'
+    assert note['message']['text'] == "did you mean 'color'"
+
+    # TODO: we don't yet capture fix-it hints on notes (PR other/116164)
+    assert 'fixes' not in result
diff --git a/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c
new file mode 100644
index 000000000000..19fa7c1b46d6
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-note-with-fix-it-hint.c
@@ -0,0 +1,69 @@ 
+/* Example of a grouped error and note, with a fix-it hint on the note.
+
+   Intended output is similar to:
+   
+/PATH/test-note-with-fix-it-hint.c:21:13: error: unknown field 'colour'
+   21 |   return p->colour;
+      |             ^~~~~~
+/PATH/test-note-with-fix-it-hint.c:21:13: note: did you mean 'color'
+   21 |   return p->colour;
+      |             ^~~~~~
+      |             color
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/* Placeholder source:
+_________11111111112
+12345678901234567890
+  return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-note-with-fix-it-hint.c.exe",
+	      "test-note-with-fix-it-hint.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_token
+    = make_range (diag_mgr, main_file, line_num, 13, 18);
+
+  diagnostic_manager_begin_group (diag_mgr);
+
+  diagnostic *err = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (err, loc_token);
+  diagnostic_finish (err, "unknown field %qs", "colour");
+
+  diagnostic *n = diagnostic_begin (diag_mgr, DIAGNOSTIC_LEVEL_NOTE);
+  diagnostic_set_location (n, loc_token);
+  diagnostic_add_fix_it_hint_replace (n, loc_token, "color");
+  diagnostic_finish (n, "did you mean %qs", "color");
+
+  diagnostic_manager_end_group (diag_mgr);
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-note-with-fix-it-hint.c:21:13: error: unknown field 'colour'" }
+   { dg-begin-multiline-output "" }
+   21 |   return p->colour;
+      |             ^~~~~~
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-note-with-fix-it-hint.c:21:13: note: did you mean 'color'" }
+   { dg-begin-multiline-output "" }
+   21 |   return p->colour;
+      |             ^~~~~~
+      |             color
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-note-with-fix-it-hint.c "test-note-with-fix-it-hint-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c b/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c
new file mode 100644
index 000000000000..c1468553a122
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-text-sink-options.c
@@ -0,0 +1,59 @@ 
+/* Example of controlling options for text sinks,
+   with multiple text sinks,
+   and color output.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  diagnostic_text_sink *sink_1
+    = diagnostic_manager_add_text_sink (diag_mgr, stderr,
+					DIAGNOSTIC_COLORIZE_NO);
+  diagnostic_text_sink_set_source_printing_enabled (sink_1, 0);
+
+  diagnostic_text_sink *sink_2
+    = diagnostic_manager_add_text_sink (diag_mgr, stderr,
+					DIAGNOSTIC_COLORIZE_YES);
+  diagnostic_text_sink_set_labelled_source_colorization_enabled (sink_2, 0);
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+  return 0;
+};
+
+/* Verify the output from text sink 1.  */
+/* { dg-regexp "\[^\n\r\]+test-text-sink-options.c:10:8: error: can't find 'foo'" } */
+
+/* Verify the output from text sink 2.
+   { dg-regexp "\[^\n\r\]+test-text-sink-options.c:10:8:" }
+   { dg-begin-multiline-output "" }
+ error: can't find 'foo'
+   10 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-c.py b/gcc/testsuite/libdiagnostics.dg/test-warning-c.py
new file mode 100644
index 000000000000..77500d72a14f
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-warning-c.py
@@ -0,0 +1,54 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 17
+expected_file_name = 'test-warning.c'
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == True
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'].endswith(expected_file_name)
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert 'PRINT' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'warning'
+    assert results[0]['level'] == 'warning'
+    assert results[0]['message']['text'] == "this is a warning"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py
new file mode 100644
index 000000000000..b60e807d4f75
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path-c.py
@@ -0,0 +1,108 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+final_line_num = 33
+
+line_num_call_to_PyList_New = final_line_num - 7;
+line_num_for_loop = final_line_num - 5;
+line_num_call_to_PyList_Append = final_line_num - 3;
+
+expected_file_name = 'test-warning-with-path.c'
+
+def test_sarif_output_for_warning_with_path(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == expected_file_name + '.exe'
+
+    results = run['results']
+    assert len(results) == 1
+
+    result = results[0]
+    assert result['ruleId'] == 'warning'
+    assert result['level'] == 'warning'
+    assert result['message']['text'] \
+        == "passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter"
+    assert len(result['locations']) == 1
+    location = result['locations'][0]
+
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'].endswith(expected_file_name)
+    assert phys_loc['region']['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert phys_loc['region']['startColumn'] == 5
+    assert phys_loc['region']['endColumn'] == 30
+    assert phys_loc['contextRegion']['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == '    PyList_Append(list, item);\n'
+
+    assert len(location['logicalLocations']) == 1
+    logical_loc = location['logicalLocations'][0]
+    assert logical_loc['name'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['fullyQualifiedName'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['decoratedName'] == 'make_a_list_of_random_ints_badly'
+    assert logical_loc['kind'] == 'function'
+
+    assert len(result['codeFlows']) == 1
+    assert len(result['codeFlows'][0]['threadFlows']) == 1
+    thread_flow = result['codeFlows'][0]['threadFlows'][0]
+
+    assert len(thread_flow['locations']) == 3
+
+    tfl_0 = thread_flow['locations'][0]
+    tfl_0_loc = tfl_0['location']
+    assert get_location_artifact_uri(tfl_0_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_0_loc)['startLine'] \
+        == line_num_call_to_PyList_New
+    assert get_location_physical_region(tfl_0_loc)['startColumn'] == 10
+    assert get_location_physical_region(tfl_0_loc)['endColumn'] == 23
+    assert get_location_snippet_text(tfl_0_loc) \
+        == '  list = PyList_New(0);\n'
+    assert tfl_0_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_0_loc['message']['text'] \
+        == "when 'PyList_New' fails, returning NULL"
+    assert tfl_0['nestingLevel'] == 0
+    assert tfl_0['executionOrder'] == 1
+
+    tfl_1 = thread_flow['locations'][1]
+    tfl_1_loc = tfl_1['location']
+    assert get_location_artifact_uri(tfl_1_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_1_loc)['startLine'] \
+        == line_num_for_loop
+    assert get_location_physical_region(tfl_1_loc)['startColumn'] == 15
+    assert get_location_physical_region(tfl_1_loc)['endColumn'] == 24
+    assert get_location_snippet_text(tfl_1_loc) \
+        == '  for (i = 0; i < count; i++) {\n'
+    assert tfl_1_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_1_loc['message']['text'] \
+        == "when 'i < count'"
+    assert tfl_1['nestingLevel'] == 0
+    assert tfl_1['executionOrder'] == 2
+
+    tfl_2 = thread_flow['locations'][2]
+    tfl_2_loc = tfl_2['location']
+    assert get_location_artifact_uri(tfl_2_loc).endswith(expected_file_name)
+    assert get_location_physical_region(tfl_2_loc)['startLine'] \
+        == line_num_call_to_PyList_Append
+    assert get_location_physical_region(tfl_2_loc)['startColumn'] == 5
+    assert get_location_physical_region(tfl_2_loc)['endColumn'] == 30
+    assert get_location_snippet_text(tfl_2_loc) \
+        == '    PyList_Append(list, item);\n'
+    assert tfl_2_loc['logicalLocations'] == location['logicalLocations']
+    assert tfl_2_loc['message']['text'] \
+        == "when calling 'PyList_Append', passing NULL from (1) as argument 1"
+    assert tfl_2['nestingLevel'] == 0
+    assert tfl_2['executionOrder'] == 3
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c
new file mode 100644
index 000000000000..6f1f4736344c
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-warning-with-path.c
@@ -0,0 +1,125 @@ 
+/* Example of emitting a warning with an execution path.
+
+TODO:
+
+   Intended output is similar to:
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122222222223333333333444444444455555555556
+123456789012345678901234567890123456789012345678901234567890
+PyObject *
+make_a_list_of_random_ints_badly(PyObject *self,
+				 PyObject *args)
+{
+  PyObject *list, *item;
+  long count, i;
+
+  if (!PyArg_ParseTuple(args, "i", &count)) {
+    return NULL;
+  }
+
+  list = PyList_New(0);
+	
+  for (i = 0; i < count; i++) {
+    item = PyLong_FromLong(random());
+    PyList_Append(list, item);
+  }
+  
+  return list;
+*/
+const int final_line_num = __LINE__ - 2;
+
+const int line_num_call_to_PyList_New = final_line_num - 7;
+const int line_num_for_loop = final_line_num - 5;
+const int line_num_call_to_PyList_Append = final_line_num - 3;
+
+int
+main ()
+{
+  begin_test ("test-warning-with-path.c.exe",
+	      "test-warning-with-path.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_call_to_PyList_New
+    = make_range (diag_mgr, main_file, line_num_call_to_PyList_New, 10, 22);
+  const diagnostic_physical_location *loc_for_cond
+    = make_range (diag_mgr, main_file, line_num_for_loop, 15, 23);
+  const diagnostic_physical_location *loc_call_to_PyList_Append
+    = make_range (diag_mgr, main_file, line_num_call_to_PyList_Append, 5, 29);
+
+  const char *funcname = "make_a_list_of_random_ints_badly";
+  const diagnostic_logical_location *logical_loc
+    = diagnostic_manager_new_logical_location (diag_mgr,
+					       DIAGNOSTIC_LOGICAL_LOCATION_KIND_FUNCTION,
+					       NULL, /* parent */
+					       funcname,
+					       funcname,
+					       funcname);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_call_to_PyList_Append);
+  diagnostic_set_logical_location (d, logical_loc);
+
+  diagnostic_execution_path *path = diagnostic_add_execution_path (d);
+  
+  diagnostic_event_id alloc_event_id
+    = diagnostic_execution_path_add_event (path,
+					   loc_call_to_PyList_New,
+					   logical_loc, 0,
+					   "when %qs fails, returning NULL",
+					   "PyList_New");
+  diagnostic_execution_path_add_event (path,
+				       loc_for_cond,
+				       logical_loc, 0,
+				       "when %qs", "i < count");
+  diagnostic_execution_path_add_event (path,
+				       loc_call_to_PyList_Append,
+				       logical_loc, 0,
+				       "when calling %qs, passing NULL from %@ as argument %i",
+				       "PyList_Append", &alloc_event_id, 1);
+
+  diagnostic_finish (d,
+		     "passing NULL as argument %i to %qs"
+		     " which requires a non-NULL parameter",
+		     1, "PyList_Append");
+  
+  return end_test ();
+};
+
+/* Check the output from the text sink.
+   { dg-begin-multiline-output "" }
+In function 'make_a_list_of_random_ints_badly':
+   { dg-end-multiline-output "" }
+   { dg-regexp "\[^\n\r\]+test-warning-with-path.c:30:5: warning: passing NULL as argument 1 to 'PyList_Append' which requires a non-NULL parameter" }
+   { dg-begin-multiline-output "" }
+   30 |     PyList_Append(list, item);
+      |     ^~~~~~~~~~~~~~~~~~~~~~~~~
+  'make_a_list_of_random_ints_badly': events 1-3
+   26 |   list = PyList_New(0);
+      |          ^~~~~~~~~~~~~
+      |          |
+      |          (1) when 'PyList_New' fails, returning NULL
+   27 | 
+   28 |   for (i = 0; i < count; i++) {
+      |               ~~~~~~~~~
+      |               |
+      |               (2) when 'i < count'
+   29 |     item = PyLong_FromLong(random());
+   30 |     PyList_Append(list, item);
+      |     ~~~~~~~~~~~~~~~~~~~~~~~~~
+      |     |
+      |     (3) when calling 'PyList_Append', passing NULL from (1) as argument 1
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-warning-with-path.c "test-warning-with-path-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-warning.c b/gcc/testsuite/libdiagnostics.dg/test-warning.c
new file mode 100644
index 000000000000..4fe855e92ca9
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-warning.c
@@ -0,0 +1,65 @@ 
+/* Example of emitting a warning.
+
+   Intended output is similar to:
+
+/PATH/test-warning.c:17:8: warning: this is a warning
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+
+   along with the equivalent in SARIF.  */
+
+#include "libdiagnostics.h"
+#include "test-helpers.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  begin_test ("test-warning.c.exe",
+	      "test-warning.c.sarif",
+	      __FILE__, "c");
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr,
+							     main_file,
+							     line_num,
+							     19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_WARNING);
+  diagnostic_set_location (d, loc_range);
+
+  diagnostic_finish (d, "this is a warning");
+
+  return end_test ();
+}
+
+/* Verify the output from the text sink.
+   { dg-regexp "\[^\n\r\]+test-warning.c:17:8: warning: this is a warning" }
+   { dg-begin-multiline-output "" }
+   17 | PRINT "hello world!";
+      |        ^~~~~~~~~~~~
+   { dg-end-multiline-output "" } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-warning.c "test-warning-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py
new file mode 100644
index 000000000000..263b8f1a497d
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file-c.py
@@ -0,0 +1,55 @@ 
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+expected_line_num = 8
+
+def test_sarif_output(sarif):
+    schema = sarif['$schema']
+    assert schema == 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
+
+    version = sarif['version']
+    assert version == '2.1.0'
+
+    runs = sarif['runs']
+    run = runs[0]
+
+    tool = run['tool']
+    assert tool['driver']['name'] == 'test-write-sarif-to-file.c.exe'
+
+    invocations = run['invocations']
+    assert len(invocations) == 1
+    assert 'workingDirectory' in invocations[0]
+    assert 'startTimeUtc' in invocations[0]
+    assert invocations[0]['executionSuccessful'] == False
+    assert invocations[0]['toolExecutionNotifications'] == []
+    assert 'endTimeUtc' in invocations[0]
+
+    artifacts = run['artifacts']
+    assert len(artifacts) == 1
+    assert artifacts[0]['location']['uri'] \
+        .endswith('test-write-sarif-to-file.c')
+    assert artifacts[0]['sourceLanguage'] == 'c'
+    assert 'PRINT' in artifacts[0]['contents']['text']
+    assert artifacts[0]['roles'] == ["analysisTarget"]
+
+    results = run['results']
+    assert len(results) == 1
+    assert results[0]['ruleId'] == 'error'
+    assert results[0]['level'] == 'error'
+    assert results[0]['message']['text'] == "can't find 'foo'"
+    assert len(results[0]['locations']) == 1
+    location = results[0]['locations'][0]
+    phys_loc = location['physicalLocation']
+    assert phys_loc['artifactLocation']['uri'] \
+        .endswith('test-write-sarif-to-file.c')
+    assert phys_loc['region']['startLine'] == expected_line_num
+    assert phys_loc['region']['startColumn'] == 8
+    assert phys_loc['region']['endColumn'] == 20
+    assert phys_loc['contextRegion']['startLine'] == expected_line_num
+    assert phys_loc['contextRegion']['snippet']['text'] \
+        == 'PRINT "hello world!";\n'
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c
new file mode 100644
index 000000000000..637935e4598b
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-write-sarif-to-file.c
@@ -0,0 +1,55 @@ 
+/* Example of writing diagnostics as SARIF to a file.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *sarif_outfile = fopen ("test-write-sarif-to-file.c.sarif", "w");
+  if (!sarif_outfile)
+    return -1;
+
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+  diagnostic_manager_set_tool_name (diag_mgr, "test-write-sarif-to-file.c.exe");
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+
+  diagnostic_manager_add_sarif_sink (diag_mgr, sarif_outfile, file,
+				     DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+
+  fclose (sarif_outfile);
+
+  return 0;
+};
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-write-sarif-to-file.c "test-write-sarif-to-file-c.py" } } */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c b/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c
new file mode 100644
index 000000000000..8ad448c8c9ce
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-write-text-to-file.c
@@ -0,0 +1,47 @@ 
+/* Example of writing diagnostics in text form, but to a file, 
+   rather than stderr.  */
+
+#include "libdiagnostics.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+  FILE *outfile = fopen ("test.txt", "w");
+  if (!outfile)
+    return -1;
+
+  diagnostic_manager *diag_mgr = diagnostic_manager_new ();
+
+  diagnostic_manager_add_text_sink (diag_mgr, outfile,
+				    DIAGNOSTIC_COLORIZE_NO);
+
+  const diagnostic_file *file = diagnostic_manager_new_file (diag_mgr, __FILE__, "c");
+  const diagnostic_physical_location *loc_start
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 8);
+  const diagnostic_physical_location *loc_end
+    = diagnostic_manager_new_location_from_file_line_column (diag_mgr, file, line_num, 19);
+  const diagnostic_physical_location *loc_range
+    = diagnostic_manager_new_location_from_range (diag_mgr,
+						  loc_start,
+						  loc_start,
+						  loc_end);
+
+  diagnostic *d = diagnostic_begin (diag_mgr,
+				    DIAGNOSTIC_LEVEL_ERROR);
+  diagnostic_set_location (d, loc_range);
+  
+  diagnostic_finish (d, "can't find %qs", "foo");
+
+  diagnostic_manager_release (diag_mgr);
+
+  fclose (outfile);
+
+  return 0;
+};