← Back to team overview

kubuntu-council team mailing list archive

[Merge] ~waveform/ubuntu-manual-tests:auto-pi into ubuntu-manual-tests:master

 

Dave Jones has proposed merging ~waveform/ubuntu-manual-tests:auto-pi into ubuntu-manual-tests:master.

Requested reviews:
  Ubuntu Testcase Admins (ubuntu-testcase)

For more details, see:
https://code.launchpad.net/~waveform/ubuntu-manual-tests/+git/ubuntu-manual-tests/+merge/408427

Adds the "tools/test_case_gen" Python script for generating multiple (largely similar, but superficially different) test cases from an XML configuration, and "tools/pi_image_cases.xml" which provides a configuration for generating the test cases for the Raspberry Pi.

The final commit actually re-generates the Raspberry Pi test cases (and adds new ones to cover the Pi 400 and CM4s).
-- 
Your team Ubuntu Testcase Admins is requested to review the proposed merge of ~waveform/ubuntu-manual-tests:auto-pi into ubuntu-manual-tests:master.
diff --git a/testcases/image/1711_RaspberryPi 4 2GB Post-install b/testcases/image/1711_RaspberryPi 4 2GB Post-install
index 2b8169a..0b3e1d6 100644
--- a/testcases/image/1711_RaspberryPi 4 2GB Post-install
+++ b/testcases/image/1711_RaspberryPi 4 2GB Post-install
@@ -1,56 +1,204 @@
-This test case is to be carried out on a Raspberry Pi 4 with 2GB of RAM.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 4 2GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 4 2GB1.6-1.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2 (black)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB3 (blue)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1719_RaspberryPi 4 4GB Post-install b/testcases/image/1719_RaspberryPi 4 4GB Post-install
index 2f3f37a..7a5c5cc 100644
--- a/testcases/image/1719_RaspberryPi 4 4GB Post-install
+++ b/testcases/image/1719_RaspberryPi 4 4GB Post-install
@@ -1,56 +1,204 @@
-This test case is to be carried out on a Raspberry Pi 4 with 4GB of RAM.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 4 4GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 4 4GB3.6-3.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2 (black)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB3 (blue)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1720_RaspberryPi 4 8GB Post-install b/testcases/image/1720_RaspberryPi 4 8GB Post-install
index ae6bea9..0fef017 100644
--- a/testcases/image/1720_RaspberryPi 4 8GB Post-install
+++ b/testcases/image/1720_RaspberryPi 4 8GB Post-install
@@ -1,56 +1,204 @@
-This test case is to be carried out on a Raspberry Pi 4 with 8GB of RAM.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 4 8GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 4 8GB7.6-7.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2 (black)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB3 (blue)</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1721_RaspberryPi 3B+ Post-install b/testcases/image/1721_RaspberryPi 3B+ Post-install
index 4dd0246..650edcc 100644
--- a/testcases/image/1721_RaspberryPi 3B+ Post-install
+++ b/testcases/image/1721_RaspberryPi 3B+ Post-install
@@ -1,56 +1,175 @@
-This test case is to be carried out on a Raspberry Pi 3B+.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 3B+</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 3B+800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1722_RaspberryPi 3B Post-install b/testcases/image/1722_RaspberryPi 3B Post-install
index 32c2614..3becf05 100644
--- a/testcases/image/1722_RaspberryPi 3B Post-install
+++ b/testcases/image/1722_RaspberryPi 3B Post-install
@@ -1,56 +1,175 @@
-This test case is to be carried out on a Raspberry Pi 3B.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 3B</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 3B800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1723_RaspberryPi 3A+ Post-install b/testcases/image/1723_RaspberryPi 3A+ Post-install
index eb51cbd..b3517c5 100644
--- a/testcases/image/1723_RaspberryPi 3A+ Post-install
+++ b/testcases/image/1723_RaspberryPi 3A+ Post-install
@@ -1,56 +1,163 @@
-This test case is to be carried out on a Raspberry Pi 3A+.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 3A+</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 3A+300-500MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1724_RaspberryPi 2 Post-install b/testcases/image/1724_RaspberryPi 2 Post-install
index 349c3f4..6016fc1 100644
--- a/testcases/image/1724_RaspberryPi 2 Post-install
+++ b/testcases/image/1724_RaspberryPi 2 Post-install
@@ -1,56 +1,153 @@
-This test case is to be carried out on a Raspberry Pi 2.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 2</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 2800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With a pair of headphones with a 3.5mm jack<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the headphone jack<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the headphone jack</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1725_RaspberryPi CM3 Post-install b/testcases/image/1725_RaspberryPi CM3 Post-install
index f5b34ca..8436e61 100644
--- a/testcases/image/1725_RaspberryPi CM3 Post-install
+++ b/testcases/image/1725_RaspberryPi CM3 Post-install
@@ -1,56 +1,120 @@
-This test case is to be carried out on a Raspberry Pi Compute Module 3.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 3</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 3800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1726_RaspberryPi CM3+ Post-install b/testcases/image/1726_RaspberryPi CM3+ Post-install
index 1037693..e40773b 100644
--- a/testcases/image/1726_RaspberryPi CM3+ Post-install
+++ b/testcases/image/1726_RaspberryPi CM3+ Post-install
@@ -1,56 +1,120 @@
-This test case is to be carried out on a Raspberry Pi Compute Module 3+.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 3+</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 3+800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1727_RaspberryPi CM3+ Lite Post-install b/testcases/image/1727_RaspberryPi CM3+ Lite Post-install
index 7b82ca6..4e3c543 100644
--- a/testcases/image/1727_RaspberryPi CM3+ Lite Post-install
+++ b/testcases/image/1727_RaspberryPi CM3+ Lite Post-install
@@ -1,56 +1,120 @@
-This test case is to be carried out on a Raspberry Pi Compute Module 3+ Lite.
-Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>IoT installation media</a>
-<dl>
-During boot, verify that:
-        <dt>You see u-boot output during boot</dt>
-        <dt>You see something on the connnected monitor after boot</dt>
-
-Once logged in, verify that:
-        <dt>A connected USB keyboard works:
-            <ul>
-                <li>when connected to a USB2 (black) port</li>
-                <li>when connected to a USB3 (blue) port</li>
-            </ul>
-        </dt>
-        <dt><code>free</code> reports RAM consistent with the specifc device.</dt>
-        <dt>The ethernet adapter received an IP address.
-            <ul>
-                <li>The device responds to a ping request.</li>
-                <li>It is possible to ssh to the device.</li>
-            </ul>
-        </dt>
-        <dt>Configure wifi via netplan by creating an /etc/netplan/config.yaml file (example contents below) and running 'sudo netplan apply'. Preferably, wifi will be on a different subnet than ethernet. If that isn't possible disconnect ethernet.
-            <ul>
-                <li><code>network:</code></li>
-                <li><code>&nbsp;&nbsp;version: 2</code></li>
-                <li><code>&nbsp;&nbsp;wifis:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;wlan0:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;access-points:</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"wap_ssid_here":</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password: "wap_password_here"</code></li>
-                <li><code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dhcp4: yes</code></li>
-            </ul>
-        </dt>
-        <dt>You hear audio output from:
-            <ul>
-                <li>the 3.5mm jack</li>
-                <li>HDMI 0</li>
-                <li>HDMI 1</li>
-            </ul>
-        </dt>
-        <dt><code>sudo flash-kernel</code> produces no error messages</dt>
-        <dt><code>sudo reboot</code> reboots the device</dt>
-        <dt><code>sudo shutdown -h now</code> powers off the device</dt>
-        <dt>Performing a large (300-600mb) file copy to USB storage works
-            <ul>
-                <li>Copy the large file to USB storage</li>
-                <li><code>sync</code></li>
-                <li>Check <code>dmesg</code> for errors</li>
-                <li>Unmount and remount the USB storage</li>
-                <li>Verify the md5sum of the file</li>
-            </ul>
-        </dt>
-Boot with the 2nd HDMI port connected. Verify that you see output on the 2nd monitor instead of the 1st.
-<strong>
-    If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
-    If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 3+ Lite</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 3+ Lite800-1000MB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      Connect a USB keyboard to one of the USB2</dt>
+    <dd>
+      Verify that keys typed on the keyboard appear on the console
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1740_RaspberryPi 400 Post-install b/testcases/image/1740_RaspberryPi 400 Post-install
new file mode 100644
index 0000000..ebb52d7
--- /dev/null
+++ b/testcases/image/1740_RaspberryPi 400 Post-install
@@ -0,0 +1,171 @@
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi 4 4GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi 4 4GB3.6-3.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1741_RaspberryPi CM4 2GB Post-install b/testcases/image/1741_RaspberryPi CM4 2GB Post-install
new file mode 100644
index 0000000..c324e0e
--- /dev/null
+++ b/testcases/image/1741_RaspberryPi CM4 2GB Post-install
@@ -0,0 +1,175 @@
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 4 2GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 4 2GB1.6-1.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1742_RaspberryPi CM4 4GB Post-install b/testcases/image/1742_RaspberryPi CM4 4GB Post-install
new file mode 100644
index 0000000..ac89a01
--- /dev/null
+++ b/testcases/image/1742_RaspberryPi CM4 4GB Post-install
@@ -0,0 +1,175 @@
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 4 4GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 4 4GB3.6-3.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/testcases/image/1743_RaspberryPi CM4 8GB Post-install b/testcases/image/1743_RaspberryPi CM4 8GB Post-install
new file mode 100644
index 0000000..764237b
--- /dev/null
+++ b/testcases/image/1743_RaspberryPi CM4 8GB Post-install
@@ -0,0 +1,175 @@
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     tools/pi_image_cases.xml
+-->
+
+    <p>This test case is to be carried out on a Raspberry Pi Compute Module 4 8GB</p>
+    <p>Follow the installation steps at <a href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    <dl>
+      
+    
+    <dt>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </dt>
+    <dd>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </dd>
+  
+    <dt>
+      Run <code>sudo flash-kernel</code>
+    </dt>
+    <dd>
+      Exit code is clean (0) and no error messages are reported
+    </dd>
+  
+    <dt>
+      Run <code>sudo reboot</code>
+    </dt>
+    <dd>
+      System reboots successfully to a login prompt
+    </dd>
+  
+    <dt>
+      Run <code>sudo shutdown -h now</code>
+    </dt>
+    <dd>
+      System shuts down in a reasonable time (less than a minute)
+    </dd>
+  
+    <dt>
+      Check output of <code>free -h</code>
+    </dt>
+    <dd>
+      Reported "Mem" under "total" is consistent with a
+      Raspberry Pi Compute Module 4 8GB7.6-7.8GB</dd>
+  
+    <dt>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </dt>
+    <dd>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI0 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI0 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI0 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      With an HDMI monitor that supports audio plugged into
+      the HDMI1 output<ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the HDMI1 port<code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the HDMI1 port</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 "Jeff Wayne - War of the
+          Worlds.mp3"</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </dt>
+    <dd>Audio can be heard through the device</dd>
+  
+    <dt>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the eth0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "eth0</dd>
+  
+    <dt>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            wlan0</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the wlan0</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </dt>
+    <dd>
+      The "wlan0</dd>
+  </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  
\ No newline at end of file
diff --git a/tools/pi_image_cases.xml b/tools/pi_image_cases.xml
new file mode 100644
index 0000000..cb592fa
--- /dev/null
+++ b/tools/pi_image_cases.xml
@@ -0,0 +1,599 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<!--
+Hopefully the layout of this file should be fairly self-explanatory,
+so you may just wish to read the content to get a feel for it, but to
+avoid any ambiguity, here's the long explanation in case it isn't:
+
+The namespace used for all the custom tags is "com.ubuntu.tests" which
+is aliased to the "ut:" shortname at the top, so wherever you see a tag
+with a "ut:" prefix, it's part of the configuration. All unprefixed tags
+are HTML to include in the output.
+
++ The outermost element must be "ut:configuration"
+|
+|-+ Directly under "ut:configuration" there must be one, and only one,
+|   "ut:template" element. This contains the HTML used for all generated
+|   test cases. Within the HTML you must include a *single* "ut:tests"
+|   element which indicates the place within the template to include
+|   the various tests for a given case.
+|
+|-+ Directly under "ut:configuration", there must be one or more
+| | "ut:test" elements. These define the individual tests to be carried
+| | out by testers. Each "ut:test" element must have an "id" attribute
+| | which uniquely identifies it; the identifier is any arbitrary string
+| | and is not directly used in the output.
+| |
+| |-+ Each "ut:test" element must contain a single "ut:action" element,
+| |   which contains the HTML describing the test to carry out
+| |   (e.g. "click this button and fill out a form")
+| |
+| \-+ Each "ut:test" element must also contain a single "ut:expected"
+|     element, which contains the HTML describing the expected outcome
+|     of the test (e.g. "a sheet of paper is printed").
+|
+\-+ Directly under "ut:configuration", there must be one or more "ut:case"
+  | elements. Each of these defines a single test-case file to be written
+  | to the output directory. Each "ut:case" element must have an "id"
+  | attribute which uniquely identifies it; the identifier will be used
+  | as the output filename for that case.
+  |
+  |-+ Each "ut:case" element must contain one or more "ut:include"
+  |   elements. Each "ut:include" element must have a "ref" attribute
+  |   naming the "id" attribute of the "ut:test" element to include
+  |   in that case. Each test will be included in the output in the
+  |   order specified.
+  |
+  \-+ Each "ut:case" element may contain zero or more "ut:define"
+      elements. Each "ut:define" element must have a "name" attribute. The
+      content of the "ut:define" element is HTML that will be substituted
+      wherever a "ut:var" element (see below) exists with a matching
+      "name" attribute. The name is an arbitrary string and is not otherwise
+      used in the output.
+
+As the last item above hints, within any section of HTML you may include
+a "ut:var" element. The (mandatory) "name" attribute names the variable
+to substitute in that position. Variables are created with "ut:define"
+elements which may exist directly under "ut:configuration" (in which
+case they apply universally), within a "ut:case" element (in which case
+they apply to all tests included in that case), or within a "ut:include"
+element (in which case they apply to just that included test).
+
+If a variable with the same name is defined in multiple contexts,
+e.g. within a "ut:case" and a "ut:include" within that case, the "inner"
+definition overrides the outer.
+-->
+
+<ut:configuration xmlns:ut="com.ubuntu.tests">
+  <ut:template>
+    <p>This test case is to be carried out on a <ut:var name="model" />.</p>
+    <p>Follow the installation steps at <a
+      href="https://ubuntu.com/download/iot/installation-media";>
+      IoT installation media</a>
+    </p>
+    <ut:var name="post-install" />
+    <dl>
+      <ut:tests />
+    </dl>
+    <p>If <strong>all</strong> actions produce the expected results listed,
+      please <a href="results#add_result">submit</a> a 'passed' result.</p>
+    <p>If <strong>any</strong> action fails, or produces an unexpected result,
+      please <a href="results#add_result">submit</a> a 'failed' result and <a
+      href="../../buginstructions">file a bug</a>. Please be sure to include
+      the bug number when you <a href="results#add_result">submit</a> your
+      result.</p>
+  </ut:template>
+
+  <ut:define name="post-install"></ut:define>
+
+  <ut:test id="uboot-video">
+    <ut:action>
+      Immediately after the "rainbow" splash screen, U-Boot starts. Press a key
+      to interrupt the sequence, then type "boot" to continue it.
+    </ut:action>
+    <ut:expected>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the primary monitor connected to the Pi, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="uboot-serial">
+    <ut:action>
+      During the boot sequence, U-Boot starts interacting via serial (GPIO14
+      and GPIO15, pins 8 and 10 respectively, on the 40-pin GPIO header). Press
+      a key to interrupt the sequence, then type "boot" to continue it.
+    </ut:action>
+    <ut:expected>
+      Check that the U-Boot prompt (including a two-second wait) appears on
+      the attached serial console, that the keyboard will interrupt
+      the sequence, and that boot successfully concludes after resumption.
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="flash-kernel">
+    <ut:action>
+      Run <code>sudo flash-kernel</code>
+    </ut:action>
+    <ut:expected>
+      Exit code is clean (0) and no error messages are reported
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="reboot">
+    <ut:action>
+      Run <code>sudo reboot</code>
+    </ut:action>
+    <ut:expected>
+      System reboots successfully to a login prompt
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="shutdown">
+    <ut:action>
+      Run <code>sudo shutdown -h now</code>
+    </ut:action>
+    <ut:expected>
+      System shuts down in a reasonable time (less than a minute)
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="usb-file-transfer">
+    <ut:action>
+      Perform a large (300-600MB) file copy to USB storage
+      <ul>
+        <li>Generate a large (500MB) file: <code>dd if=/dev/urandom of=rubbish
+          bs=1M count=500</code></li>
+        <li>Insert a USB stick (appropriately sized) into a spare USB port</li>
+        <li>Make a mount directory: <code>sudo mkdir /mnt/stick</code></li>
+        <li>Mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (modify mount-point as necessary; check <code>sudo dmesg</code>
+          output if unsure)</li>
+        <li>Copy the file: <code>sudo cp rubbish /mnt/stick/</code></li>
+        <li>Unmount the stick: <code>sudo umount /mnt/stick</code></li>
+        <li>Remove the stick from the USB port</li>
+        <li>Re-insert the stick into the USB port</li>
+        <li>Re-mount the stick: <code>sudo mount /dev/sda1 /mnt/stick</code>
+          (again, adjust mount-point as necessary)</li>
+        <li>Compare the copied file to that on the stick: <code>cmp rubbish
+          /mnt/stick/rubbish</code></li>
+      </ul>
+    </ut:action>
+    <ut:expected>
+      <code>cmp</code> returns 0 and outputs nothing indicating the files are
+      identical
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="usb-keyboard">
+    <ut:action>
+      Connect a USB keyboard to one of the <ut:var name="usb" /> ports
+    </ut:action>
+    <ut:expected>
+      Verify that keys typed on the keyboard appear on the console
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="ram-free">
+    <ut:action>
+      Check output of <code>free -h</code>
+    </ut:action>
+    <ut:expected>
+      Reported &quot;Mem&quot; under &quot;total&quot; is consistent with a
+      <ut:var name="model" />. It should be in the region of <ut:var name="mem"
+      />.
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="dual-monitor">
+    <ut:action>
+      Boot with a monitor attached to both HDMI ports
+    </ut:action>
+    <ut:expected>
+      Verify that you see output on both monitors
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="ethernet">
+    <ut:action>
+      Check auto-configuration of ethernet
+      <ul>
+        <li>Run <code>ip addr</code></li>
+        <li>Check that a valid IP address is recorded on the <ut:var
+           name="intf" /> interface</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </ut:action>
+    <ut:expected>
+      The &quot;<ut:var name="intf" />&quot; interface should have a DHCP
+      assigned IP address and you should be able to ping google.com
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="wifi">
+    <ut:action>
+      Configure wifi via netplan
+      <ul>
+        <li>Place the following in <code>/etc/netplan/wifi.yaml</code>
+        (substituting the SSID and password as necessary):</li>
+        <li><pre>
+      network:
+        version: 2
+          wifis:
+            <ut:var name="intf" />:
+              dhcp4: true
+              access-points:
+                my-ssid-here:
+                  password: my-password-here</pre>
+        </li>
+        <li>Run <code>sudo netplan apply</code></li>
+        <li>Wait a few seconds (to allow DHCP to complete), then run <code>ip
+          addr</code></li>
+        <li>Check that a valid IP address is recorded on the <ut:var
+          name="intf" /> interface</li>
+        <li>Check <code>ping google.com</code> successfully pings a few times
+          (<tt>Ctrl+C</tt> to cancel)</li>
+      </ul>
+    </ut:action>
+    <ut:expected>
+      The &quot;<ut:var name="intf" />&quot; interface should have a DHCP
+      assigned IP address and you should be able to ping google.com
+    </ut:expected>
+  </ut:test>
+
+  <ut:test id="audio">
+    <ut:action>
+      With <ut:var name="device" />, and an available MP3 file:
+      <ul>
+        <li>Install mpg321 and amixer with <code>sudo apt install mpg321
+          alsa-utils</code></li>
+        <li>Find the correct hardware output for the <ut:var name="output" />:
+          <code>cat /proc/asound/cards</code> and note the number at the start
+          of the line for the <ut:var name="output" /> (usually 0 and possibly
+          1 for any connected monitor(s), and 1 or possibly 2 for the headphone
+          jack)</li>
+        <li>Attempt to play your MP3 file with: <code>mpg321 -o alsa -a
+          hw:<em>num</em>,0 <em>music.mp3</em></code> substituting
+          <em>num</em> for the number found during the previous step, and
+          <em>music.mp3</em> for your choice of MP3 file, e.g. <code>mpg321 -o
+          alsa -a hw:0,0 &quot;Jeff Wayne - War of the
+          Worlds.mp3&quot;</code></li>
+        <li>Use <tt>Ctrl+C</tt> to end playback early, if you wish</li>
+        <li>If you cannot hear anything, first check that the mixer's volume is
+          not set too low; run <code>alsamixer</code>, and adjust the volume
+          (<tt>J</tt> for down, <tt>K</tt> for up) before exiting
+          (<tt>Esc</tt>) and retrying playback</li>
+      </ul>
+    </ut:action>
+    <ut:expected>Audio can be heard through the device</ut:expected>
+  </ut:test>
+
+  <ut:case id="1711_RaspberryPi 4 2GB Post-install">
+    <ut:define name="model">Raspberry Pi 4 2GB</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">1.6-1.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2 (black)</ut:define></ut:include>
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB3 (blue)</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1719_RaspberryPi 4 4GB Post-install">
+    <ut:define name="model">Raspberry Pi 4 4GB</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">3.6-3.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2 (black)</ut:define></ut:include>
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB3 (blue)</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1720_RaspberryPi 4 8GB Post-install">
+    <ut:define name="model">Raspberry Pi 4 8GB</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">7.6-7.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2 (black)</ut:define></ut:include>
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB3 (blue)</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1721_RaspberryPi 3B+ Post-install">
+    <ut:define name="model">Raspberry Pi 3B+</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1722_RaspberryPi 3B Post-install">
+    <ut:define name="model">Raspberry Pi 3B</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1723_RaspberryPi 3A+ Post-install">
+    <ut:define name="model">Raspberry Pi 3A+</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">300-500MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1724_RaspberryPi 2 Post-install">
+    <ut:define name="model">Raspberry Pi 2</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">a pair of headphones with a 3.5mm jack</ut:define>
+      <ut:define name="output">headphone jack</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1725_RaspberryPi CM3 Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 3</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+  </ut:case>
+
+  <ut:case id="1726_RaspberryPi CM3+ Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 3+</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+  </ut:case>
+
+  <ut:case id="1727_RaspberryPi CM3+ Lite Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 3+ Lite</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">800-1000MB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="usb-keyboard"><ut:define name="usb">USB2</ut:define></ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio</ut:define>
+      <ut:define name="output">HDMI port</ut:define>
+    </ut:include>
+  </ut:case>
+
+  <ut:case id="1740_RaspberryPi 400 Post-install">
+    <ut:define name="model">Raspberry Pi 4 4GB</ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">3.6-3.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1741_RaspberryPi CM4 2GB Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 4 2GB</ut:define>
+    <ut:define name="post-install">
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    </ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">1.6-1.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1742_RaspberryPi CM4 4GB Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 4 4GB</ut:define>
+    <ut:define name="post-install">
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    </ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">3.6-3.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+
+  <ut:case id="1743_RaspberryPi CM4 8GB Post-install">
+    <ut:define name="model">Raspberry Pi Compute Module 4 8GB</ut:define>
+    <ut:define name="post-install">
+      <p>Before booting your CM4 with the new image, edit config.txt on the boot
+      (1st) partition and uncomment the <code>#dtoverlay=dwc2,dr_mode=host</code>
+      line to ensure the USB ports on the IO board operate correctly</p>
+    </ut:define>
+    <ut:include ref="uboot-video" />
+    <ut:include ref="uboot-serial" />
+    <ut:include ref="flash-kernel" />
+    <ut:include ref="reboot" />
+    <ut:include ref="shutdown" />
+    <ut:include ref="ram-free"><ut:define name="mem">7.6-7.8GB</ut:define></ut:include>
+    <ut:include ref="usb-file-transfer" />
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI0 output</ut:define>
+      <ut:define name="output">HDMI0 port</ut:define>
+    </ut:include>
+    <ut:include ref="audio">
+      <ut:define name="device">an HDMI monitor that supports audio plugged into
+      the HDMI1 output</ut:define>
+      <ut:define name="output">HDMI1 port</ut:define>
+    </ut:include>
+    <ut:include ref="ethernet"><ut:define name="intf">eth0</ut:define></ut:include>
+    <ut:include ref="wifi"><ut:define name="intf">wlan0</ut:define></ut:include>
+  </ut:case>
+</ut:configuration>
diff --git a/tools/test_case_gen b/tools/test_case_gen
new file mode 100755
index 0000000..24984c4
--- /dev/null
+++ b/tools/test_case_gen
@@ -0,0 +1,260 @@
+#!/usr/bin/python3
+
+"""
+A script for generating test cases from an XML definition. At present, the test
+cases for the Raspberry Pi images involve a large amount of duplication, but
+with minor variances. For example, the tests for the Pi 2 don't include wifi,
+the Pi 3A+ has no ethernet, Pi 4 devices have two HDMI ports to test, and so
+on. This script generates the test cases from an XML definition that permits
+easy configuration of which tests to include or exclude, duplication of tests,
+and variable substitution at a template, test-case, or individual test level.
+
+The script must be run with an XML definition; it will re-generate all the test
+cases specified in the given definition, writing the output to the configured
+directory.
+"""
+
+import os
+import re
+import sys
+import argparse
+from copy import deepcopy
+from pathlib import Path
+from xml.etree import ElementTree as et
+
+
+class ut:
+    """
+    A trivial class to define the XML namespace used in the configuration file
+    and the (expanded) names of all the tags specific to that namespace.
+    """
+    ns       = 'com.ubuntu.tests'
+    config   = f'{{{ns}}}configuration'
+    template = f'{{{ns}}}template'
+    tests    = f'{{{ns}}}tests'
+    test     = f'{{{ns}}}test'
+    case     = f'{{{ns}}}case'
+    define   = f'{{{ns}}}define'
+    include  = f'{{{ns}}}include'
+    var      = f'{{{ns}}}var'
+    action   = f'{{{ns}}}action'
+    expected = f'{{{ns}}}expected'
+
+
+def main(args=None):
+    if sys.version_info < (3, 6):
+        raise SystemExit('This script requires Python 3.6 or later')
+
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        'config', type=argparse.FileType('r'), metavar='XML-FILE',
+        help="The configuration file to generate test cases from")
+    parser.add_argument(
+        '-o', '--output', default='testcases/image/', type=Path, metavar='DIR',
+        help="The output directory; default: %(default)s")
+    parser.add_argument(
+        '--action-tag', default='dt', metavar='TAG',
+        help="The HTML tag to wrap <ut:action> content in; default: "
+        "%(default)s")
+    parser.add_argument(
+        '--expected-tag', default='dd', metavar='TAG',
+        help="The HTML tag to wrap <ut:expected> content in; default: "
+        "%(default)s")
+
+    ns = parser.parse_args(args)
+
+    try:
+        for filename, doc in TestConfiguration(
+                ns.config,
+                action_tag=ns.action_tag,
+                expected_tag=ns.expected_tag):
+            content = et.tostring(doc, encoding='unicode', method='html')
+            # Hack off the outer elements as the result is effectively an
+            # incomplete snippet of HTML. We can't do this with etree as
+            # it makes the result invalid XML, so just fudge it with some
+            # regexes
+            content = re.sub(r'^\s*<(?:\w+:)?template[^>]*>', '', content)
+            content = re.sub(r'</(?:\w+:)?template>\s*$', '', content)
+            content = f"""\
+<!-- Please do not edit this file directly; it was generated with the
+     tools/test_case_gen script using the following configuration as input:
+     {ns.config.name}
+-->
+""" + content
+            with open(ns.output / filename, 'w', encoding='utf-8') as f:
+                f.write(content)
+    except:
+        if int(os.environ.get('DEBUG', '0')):
+            raise
+        else:
+            typ, value, tb = sys.exc_info()
+            print(value, file=sys.stderr)
+            sys.exit(1)
+
+
+def substitute(doc, path, content):
+    """
+    Given *doc*, an :class:`~xml.etree.ElementTree.Element` and *path*, an
+    `XPath`_ search string, the function finds all elements matching *path* and
+    replaces them with the contents of *content*, another
+    :class:`~xml.etree.ElementTree.Element` instance. The resulting document
+    is returned (the original *doc* is not modified).
+
+    *content* can also be a callable. In this case, for each element matching
+    *path*, the *content* callable will be called with *doc* and the matching
+    element. It must return a single :class:`~xml.etree.ElementTree.Element`
+    containing the content to substitute.
+
+    .. _XPath: https://docs.python.org/3/library/xml.etree.elementtree.html#elementtree-xpath
+
+    .. note::
+
+        Note that the *contents* elements themselves are *not* included in the
+        substitution. Only the descendents (including text, child elements,
+        etc.) of *contents* are substituted, and the substitution replaces the
+        placeholder element located by *path* entirely.
+    """
+    doc = deepcopy(doc)
+    parent_map = {
+        elem: parent
+        for parent in doc.iter()
+        for elem in parent
+    }
+    if not callable(content):
+        content_fun = lambda doc, elem: content
+    else:
+        content_fun = content
+    for placeholder in doc.findall(path):
+        content = content_fun(doc, placeholder)
+        parent = parent_map[placeholder]
+        parent_index = list(parent).index(placeholder)
+        if content.text is not None:
+            if parent_index == 0:
+                if parent.text is None:
+                    parent.text = content.text
+                else:
+                    parent.text += content.text
+            else:
+                prior = parent[parent_index - 1]
+                if prior.tail is None:
+                    prior.tail = content.text
+                else:
+                    prior.tail += content.text
+        for elem in content:
+            parent.insert(parent_index, elem)
+            parent_index += 1
+        parent.remove(placeholder)
+    return doc
+
+
+class TestConfiguration:
+    """
+    Given *xml_file*, a file-like object containing a compatible XML
+    configuration for this script, this object will act as an iterator which
+    yields tuples of (filename, xml-document), where "xml-document" is the
+    :class:`~xml.etree.ElementTree.Element` at the root of the generated test
+    case document.
+    """
+    def __init__(self, xml_file, *, action_tag='dt', expected_tag='dd'):
+        super().__init__()
+        xml_config = et.fromstring(xml_file.read())
+        if xml_config.tag != ut.config:
+            raise ValueError(
+                'Expected to find <ut:config> as the root element')
+        self._template = xml_config.find(f'./{ut.template}')
+        if self._template is None:
+            raise ValueError(
+                'Expected to find <ut:template> under the <ut:config> '
+                'element')
+        if self._template.find(f'.//{ut.tests}') is None:
+            raise ValueError(
+                '<ut:template> must include <ut:tests> somewhere within it')
+        try:
+            self._tests = {
+                elem.attrib['id']: elem
+                for elem in xml_config.findall(f'./{ut.test}')
+            }
+        except KeyError:
+            raise ValueError(f'Missing "id" attribute in {ut.test}')
+        else:
+            if not self._tests:
+                raise ValueError(
+                    'Expected to find at least one <ut:test> under the '
+                    '<ut:config> element')
+        try:
+            self._cases = {
+                elem.attrib['id']: elem
+                for elem in xml_config.findall(f'./{ut.case}')
+            }
+        except KeyError:
+            raise ValueError('Missing "id" attribute in <ut:case>')
+        else:
+            if not self._cases:
+                raise ValueError(
+                    'Expected to find at least one <ut:case> under the '
+                    '<ut:config> element')
+        self._action_tag = str(action_tag)
+        self._expected_tag = str(expected_tag)
+        self._env = self._build_env(root=xml_config)
+
+    def _build_env(self, *, root):
+        try:
+            return {
+                elem.attrib['name']: elem
+                for elem in root.findall(f'./{ut.define}')
+            }
+        except KeyError:
+            raise ValueError('Missing "name" attribute in <ut:define>')
+
+    def __iter__(self):
+        for case_id, case in self._cases.items():
+            try:
+                yield case_id, self._render_case(case)
+            except ValueError as exc:
+                raise ValueError(f'In <ut:case id={case_id!r}>: {exc}')
+
+    def _render_case(self, case):
+        env = self._env.copy()
+        env.update(self._build_env(root=case))
+        render = lambda doc, elem: self._render_var(doc, elem, env)
+        return substitute(
+            substitute(self._template, f'.//{ut.var}', render),
+            f'.//{ut.tests}', self._render_includes(case, env))
+
+    def _render_includes(self, case, env):
+        result = deepcopy(case)
+        for define in result.findall(f'./{ut.define}'):
+            result.remove(define)
+        render = lambda doc, elem: self._render_include(doc, elem, env)
+        return substitute(result, f'./{ut.include}', render)
+
+    def _render_include(self, case, include, env):
+        try:
+            test_ref = include.attrib['ref']
+        except KeyError:
+            raise ValueError('Missing "ref" attribute in <ut:include>')
+        try:
+            test = self._tests[test_ref]
+        except KeyError:
+            raise ValueError(f'<ut:test id={test_ref!r}> not found')
+        env = env.copy()
+        env.update(self._build_env(root=include))
+        result = deepcopy(test)
+        result.find(f'./{ut.action}').tag = self._action_tag
+        result.find(f'./{ut.expected}').tag = self._expected_tag
+        render = lambda doc, elem: self._render_var(doc, elem, env)
+        return substitute(result, f'.//{ut.var}', render)
+
+    def _render_var(self, include, var, env):
+        try:
+            var_name = var.attrib['name']
+        except KeyError:
+            raise ValueError('Missing "name" attribute in <ut:var>')
+        try:
+            return env[var_name]
+        except KeyError:
+            raise ValueError(f'<ut:define name={var_name!r}> not found')
+
+
+if __name__ == '__main__':
+    main()

Follow ups