Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# The boot-test job then boots every IRIX image on the Linux, macOS and Windows
# CLI builds: a platform x irix matrix, each image listed once and pinned by URL
# and sha256. IRIX 5.3 boots once on the serial console and once on the
# graphics console; IRIX 6.5.22 on the serial console only.
# graphics console; IRIX 6.5.22 and 6.5.30 on the serial console only. The
# 6.5.30 image is an O2, which has no PROM built in, so its entry pins one too.
#
# Runner images are pinned: a glibc-linked Linux binary needs at least the
# build host's glibc, so ubuntu-24.04 decides which distros our packages run on,
Expand Down Expand Up @@ -405,6 +406,10 @@ jobs:
# Stock IRIX 6.5.22 Indy install, 865 MB. Serial console only: there is
# no golden screenshot of this disk.
- { version: "6.5.22", url: "https://origami-dist.irix.fans/irix/6.5.22/indy-a22c2ba9.chd", sha256: a22c2ba9abc3bd6c61056304c965c39074c0ef00e06f7c9aabd1ec25a1865482, steps: tests/irix/boot-irix65.toml }
# Stock IRIX 6.5.30 O2 install, 879 MB, on the O2's rev 4.18 PROM (the
# IRIX installer's ip32prom.image). Serial console only, as above. The
# others leave machine and the PROM empty and boot an Indy.
- { version: "6.5.30", url: "https://origami-dist.irix.fans/irix/6.5.30/o2-34c49a43.chd", sha256: 34c49a43a794e1391f36eab2cb6e10115dbbbe39e5b4a2d0cc6512d47ff235fc, steps: tests/irix/boot-irix6530-o2.toml, machine: o2_ip32, prom_url: "https://origami-dist.irix.fans/prom/ip32prom-4.18.image", prom_sha256: 11230c353cb14fcc50ede286df228e169e32fc9e5a2206276ae29c259fafb23c }
defaults:
run:
shell: bash
Expand Down Expand Up @@ -449,8 +454,18 @@ jobs:
fi
ls -la boot-image

- name: Download the PROM image
if: ${{ matrix.irix.prom_url }}
run: |
curl -fL --retry 3 --retry-delay 10 -o boot-image/prom.image "${{ matrix.irix.prom_url }}"
got=$({ shasum -a 256 2>/dev/null || sha256sum; } < boot-image/prom.image | cut -d' ' -f1)
if [ "$got" != "${{ matrix.irix.prom_sha256 }}" ]; then
echo "::error::boot-image/prom.image sha256 $got, expected ${{ matrix.irix.prom_sha256 }}"
exit 1
fi

- name: Boot IRIX ${{ matrix.irix.version }}
run: core/iris-ci boot-test boot-image/disk.chd ${{ matrix.irix.steps && format('--steps {0}', matrix.irix.steps) || '' }} --out boot-test --timeout "$BOOT_TIMEOUT"
run: core/iris-ci boot-test boot-image/disk.chd ${{ matrix.irix.steps && format('--steps {0}', matrix.irix.steps) || '' }} ${{ matrix.irix.machine && format('--machine {0} --prom boot-image/prom.image', matrix.irix.machine) || '' }} --out boot-test --timeout "$BOOT_TIMEOUT"

# boot-test writes each check as a JUnit case, so a failed run is
# annotated with the check that failed.
Expand Down
7 changes: 4 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,11 @@ space; a skipped test (`# SKIP`) reports `ok`.

`build.yml` boots every IRIX image with each CLI build, on Linux, macOS and
Windows: a platform by image matrix in which each image is listed once, from a
pinned, checksummed stock Indy disk. IRIX 5.3 boots on the serial and graphics
pinned, checksummed stock disk. IRIX 5.3 boots on the serial and graphics
consoles ([clanker/testing/ci-irix53-disk.md](clanker/testing/ci-irix53-disk.md));
IRIX 6.5.22 boots on the serial console only, for want of a golden screenshot.
6.5.30 has no Indy disk published, so it is not booted.
IRIX 6.5.22 and 6.5.30 boot on the serial console only, for want of a golden
screenshot. No 6.5.30 Indy disk is published, so 6.5.30 boots an O2 (`--machine
o2_ip32`) on its own PROM image, pinned in the matrix.

`iris-ci boot-test` seeds NVRAM on a first headless run, boots with the console
on serial and waits up to 15 minutes for `login:`, then does the same with the
Expand Down
7 changes: 7 additions & 0 deletions clanker/testing/ci-unattended-irix-boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ directory; `--steps` and `--golden` replace them. It starts its own iris, so
run it as `iris-ci boot-test <disk> --out <dir>` with `iris` beside `iris-ci`
or named with `--iris`.

`--machine` names the profile the disk was installed on (`indy_ip24` unless set)
and `--prom` the PROM image for one with none built in. The O2 has none, and its
Command Monitor prompts with `> ` where the Indy's prints `>> `. Its settings
live in the PROM's flash, which `nvram` names and `rtc-save` writes. The stock
6.5.30 O2 disk (`tests/irix/boot-irix6530-o2.toml`) reached `login:` on serial
170 s after start, on a loaded 16-thread host.

## A fresh NVRAM can't boot anything

With no `nvram.bin` (or a bad checksum) the PROM prints `NVRAM checksum is
Expand Down
5 changes: 5 additions & 0 deletions src/ip32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,11 @@ impl Ip32Bus {
self.crime.regs.interrupting()
}

/// The file the flash is kept in, if one was named.
pub fn flash_path(&self) -> Option<&std::path::Path> {
self.flash_file.as_deref()
}

/// Write the flash out, if a file was named for it.
pub fn save_flash(&self) {
let Some(path) = &self.flash_file else { return };
Expand Down
27 changes: 27 additions & 0 deletions src/ip32_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,17 @@ impl Workstation for O2Machine {
self.controller_up.store(true, Ordering::SeqCst);
}
fn monitor_addr(&self) -> Option<std::net::SocketAddr> { self.monitor.bound_addr() }
/// The PROM's flash, where `setenv` keeps its settings, and the RTC's RAM,
/// written out now. The flash goes only to the file `nvram` names.
fn save_nvram(&self, path: Option<&str>) -> Result<String, String> {
if path.is_some() {
return Err("the O2 saves its flash only to the file `nvram` names".into());
}
let file = self.bus.flash_path().ok_or("no nvram file configured")?;
self.bus.save_flash();
self.bus.save_rtc();
Ok(file.display().to_string())
}
fn lifecycle(&self) -> crate::machine_state::GuestState { self.lifecycle }
fn set_lifecycle(&mut self, state: crate::machine_state::GuestState) { self.lifecycle = state; }

Expand Down Expand Up @@ -1292,6 +1303,22 @@ mod drop_tests {
let _ = std::fs::remove_dir_all(&dir);
}

/// `iris-ci rtc-save` on an O2 writes the PROM's flash to the `nvram`
/// file now, rather than failing as a machine with no NVRAM does.
#[test]
fn rtc_save_writes_the_flash_to_the_nvram_file() {
let dir = scratch("o2-save-nvram");
let cfg = config(&dir, MachineProfile::O2Ip32, GraphicsBoard::Newport);
let nvram = std::path::PathBuf::from(&cfg.nvram);
let machine = big_stack(move || O2Machine::new(&cfg).expect("O2"));
assert!(!nvram.exists());
assert_eq!(Workstation::save_nvram(&machine, None), Ok(nvram.display().to_string()));
assert_eq!(std::fs::metadata(&nvram).unwrap().len(), PROM_SIZE as u64);
assert!(Workstation::save_nvram(&machine, Some("elsewhere.bin")).is_err());
drop(machine);
let _ = std::fs::remove_dir_all(&dir);
}

/// `[nvme]` puts a card in the "PCI option" slot and records its disk;
/// the bus's disk flush reaches its namespace.
#[test]
Expand Down
76 changes: 69 additions & 7 deletions src/iris_ci_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ pub struct Args {
/// A screenshot to match in place of the one each graphics boot names.
#[arg(long)]
golden: Option<PathBuf>,
/// The machine the disk was installed on, by its `[machine] profile` name.
/// The disk is a stock install for one machine and boots only there.
#[arg(long, default_value = "indy_ip24")]
machine: String,
/// The PROM image, for a machine with no PROM built in (the O2).
#[arg(long)]
prom: Option<PathBuf>,
/// Seconds each boot may take to reach its login.
#[arg(long, default_value_t = 900)]
timeout: u64,
Expand Down Expand Up @@ -96,6 +103,15 @@ pub fn run(args: Args) -> Result<()> {
});
}
let abs = |p: &Path| std::path::absolute(p).map_err(|e| local(p, e));
let machine = iris::config::MachineProfile::from_name(&args.machine).map_err(Error::Local)?;
let prom = match (&args.prom, machine.has_embedded_prom()) {
(Some(p), _) => Some(abs(p)?),
(None, true) => None,
(None, false) => return Err(Error::Local(format!("{} has no PROM built in: name one with --prom", machine.name()))),
};
if let Some(p) = prom.as_ref().filter(|p| !p.is_file()) {
return Err(Error::Local(format!("{}: no such PROM image", p.display())));
}
let disk = abs(&args.disk)?;
if !disk.is_file() {
return Err(Error::Local(format!("{}: no such disk image", disk.display())));
Expand All @@ -115,13 +131,14 @@ pub fn run(args: Args) -> Result<()> {
}

let socket = ci_socket();
std::fs::write(out.join("boot-test.toml"), config(&disk, &socket)).map_err(|e| local(&out, e))?;
std::fs::write(out.join("boot-test.toml"), config(&disk, &socket, machine.name(), prom.as_deref())).map_err(|e| local(&out, e))?;

let mut t = Test {
out: out.clone(),
iris,
opts: Opts { socket: PathBuf::from(socket), json: false, quiet: true },
child: None,
prompt: monitor_prompt(machine),
check: String::new(),
cases: Vec::new(),
failed: false,
Expand Down Expand Up @@ -153,6 +170,13 @@ fn verdict(aborted: Step, failed: bool) -> Result<()> {
}
}

/// The Command Monitor's prompt. The O2's PROM prints a single `>`; the
/// others print `>>`. Waiting for `> ` on all of them would also stop at any
/// `> ` inside a listing that precedes the prompt.
fn monitor_prompt(machine: iris::config::MachineProfile) -> &'static str {
if machine == iris::config::MachineProfile::O2Ip32 { "> " } else { ">> " }
}

/// The CI socket: TCP on Windows, and elsewhere a Unix socket in the host's
/// temporary directory, as iris's own default is. A path under the output
/// directory could pass macOS's 104-byte limit on a Unix socket path.
Expand All @@ -164,17 +188,24 @@ fn ci_socket() -> String {
}
}

/// The iris config of every run: the disk on SCSI 1 with an overlay, the
/// NVRAM and the serial mirror in the working directory.
fn config(disk: &Path, socket: &str) -> String {
/// The iris config of every run: the machine and its PROM, the disk on SCSI 1
/// with an overlay, the NVRAM (the PROM's flash on an O2) and the serial mirror
/// in the working directory.
fn config(disk: &Path, socket: &str, machine: &str, prom: Option<&Path>) -> String {
let mut scsi1 = toml::Table::new();
// Forward slashes in TOML work on every host, Windows included.
scsi1.insert("path".into(), disk.to_string_lossy().replace('\\', "/").into());
scsi1.insert("cdrom".into(), false.into());
scsi1.insert("overlay".into(), true.into());
let mut scsi = toml::Table::new();
scsi.insert("1".into(), scsi1.into());
let mut m = toml::Table::new();
m.insert("profile".into(), machine.into());
let mut t = toml::Table::new();
t.insert("machine".into(), m.into());
if let Some(p) = prom {
t.insert("prom".into(), p.to_string_lossy().replace('\\', "/").into());
}
t.insert("no_audio".into(), true.into());
t.insert("nvram".into(), "nvram.bin".into());
t.insert("serial_log".into(), "serial.log".into());
Expand Down Expand Up @@ -223,6 +254,8 @@ struct Test {
iris: PathBuf,
opts: Opts,
child: Option<Child>,
/// What the PROM's Command Monitor prints when it wants a line.
prompt: &'static str,
/// The case a failure that ends the test is filed under.
check: String,
cases: Vec<TestCase>,
Expand Down Expand Up @@ -345,7 +378,7 @@ impl Test {
/// printed comes back.
fn monitor(&mut self, line: &str) -> Step<String> {
let sent = send(&self.opts, "serial-send", json!({"data": format!("{line}\r")}));
match sent.is_ok().then(|| self.prom_wait(">> ", PROMPT_WAIT)).flatten() {
match sent.is_ok().then(|| self.prom_wait(self.prompt, PROMPT_WAIT)).flatten() {
Some(text) => Ok(text),
None => {
self.dump(&self.out.join("phase-a.log"));
Expand All @@ -372,7 +405,7 @@ impl Test {
}
// Enter Command Monitor.
let _ = send(&self.opts, "serial-send", json!({"data": "5\r"}));
if self.prom_wait(">> ", PROMPT_WAIT).is_none() {
if self.prom_wait(self.prompt, PROMPT_WAIT).is_none() {
self.dump(&self.out.join("phase-a.log"));
return Err(self.abort("phase A: no Command Monitor prompt".into()));
}
Expand Down Expand Up @@ -555,6 +588,7 @@ mod tests {
iris: PathBuf::new(),
opts: Opts { socket: PathBuf::new(), json: false, quiet: true },
child: None,
prompt: ">> ",
check: String::new(),
cases: Vec::new(),
failed: false,
Expand Down Expand Up @@ -613,11 +647,26 @@ mod tests {
#[test]
fn the_config_names_the_disk_whatever_its_path_holds() {
let disk = Path::new("C:\\a \"b\"\\irix.chd");
let t: toml::Table = toml::from_str(&config(disk, "127.0.0.1:19851")).unwrap();
let t: toml::Table = toml::from_str(&config(disk, "127.0.0.1:19851", "indy_ip24", None)).unwrap();
let scsi1 = &t["scsi"]["1"];
assert_eq!(scsi1["path"].as_str(), Some("C:/a \"b\"/irix.chd"));
assert_eq!((scsi1["overlay"].as_bool(), t["ci"].as_bool()), (Some(true), Some(true)));
assert_eq!(t["ci_socket"].as_str(), Some("127.0.0.1:19851"));
assert_eq!(t["machine"]["profile"].as_str(), Some("indy_ip24"));
assert!(!t.contains_key("prom"));
}

#[test]
fn the_o2_prom_prompts_with_one_angle_bracket() {
use iris::config::MachineProfile::*;
assert_eq!((monitor_prompt(O2Ip32), monitor_prompt(IndyIp24)), ("> ", ">> "));
}

#[test]
fn an_o2_config_names_the_machine_and_its_prom() {
let t: toml::Table = toml::from_str(&config(Path::new("d.chd"), "s", "o2_ip32", Some(Path::new(r"C:\p\ip32prom.bin")))).unwrap();
assert_eq!(t["machine"]["profile"].as_str(), Some("o2_ip32"));
assert_eq!(t["prom"].as_str(), Some("C:/p/ip32prom.bin"));
}

/// The built-in file: the same seeded variables as before, a serial and a
Expand All @@ -642,6 +691,19 @@ mod tests {
assert_eq!((label.as_str(), img.width > 0), ("indy-irix53.png", true));
}

/// The O2's file seeds the same variables as the Indy's and checks only
/// the serial login: no golden screenshot of that disk exists.
#[test]
fn the_o2_boot_is_the_serial_login_with_the_indys_seeding() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/irix");
let o2 = guest_steps::load(&dir.join("boot-irix6530-o2.toml")).unwrap();
let indy = guest_steps::parse(BOOT_TOML, "boot.toml").unwrap();
let vars = |f: &guest_steps::StepFile| f.nvram.iter().map(|v| (v.name.clone(), v.value.clone(), v.guest)).collect::<Vec<_>>();
assert_eq!(vars(&o2), vars(&indy));
let boots: Vec<_> = o2.boot.iter().map(|b| (b.console.as_str(), b.serial.as_deref(), b.screen.is_some())).collect();
assert_eq!(boots, [("d", Some("login:"), false)]);
}

#[test]
fn a_screenshot_is_found_beside_the_steps_file_or_built_in_and_not_otherwise() {
let dir = std::env::temp_dir().join(format!("iris-ci-golden-{}", std::process::id()));
Expand Down
28 changes: 28 additions & 0 deletions tests/irix/boot-irix6530-o2.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Read by `iris-ci boot-test --steps` for the stock IRIX 6.5.30 O2 disk. The
# same seeding as boot.toml, on the O2's PROM; serial console only, because no
# golden screenshot of this disk exists.

[[nvram]]
name = "SystemPartition"
value = "scsi(0)disk(1)rdisk(0)partition(8)"
guest = true

[[nvram]]
name = "OSLoadPartition"
value = "scsi(0)disk(1)rdisk(0)partition(0)"
guest = true

[[nvram]]
name = "OSLoadFilename"
value = "/unix"
guest = true

[[nvram]]
name = "AutoLoad"
value = "Yes"

[[boot]]
name = "serial console: login prompt"
console = "d"
quit = "iris quits when asked, serial boot"
serial = "login:"
Loading